tdd-cli 0.12.0__tar.gz → 0.12.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/CHANGELOG.md +18 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/PKG-INFO +1 -1
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/__init__.py +1 -1
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/adapters/cargo_adapter.py +137 -12
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_cargo_adapter.py +188 -1
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/.gitignore +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/LICENSE +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/README.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/SECURITY.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/docs/PRD.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/docs/harness-integration.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/examples/claude-code-hooks/README.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/examples/claude-code-hooks/bash_hook.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/examples/claude-code-hooks/stop_hook.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/examples/plan.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/examples/skills/tdd-drive/README.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/examples/skills/tdd-drive/SKILL.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/examples/skills/tdd-handoff/README.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/examples/skills/tdd-handoff/SKILL.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/pyproject.toml +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/adapters/__init__.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/adapters/base.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/adapters/exec_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/adapters/gradle_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/adapters/pytest_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/adapters/vitest_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/adapters/xctest_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/advance.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/cli.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/config.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/contract.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/docs.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/envelope.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/fleet.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/gitutil.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/identity.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/leases.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/ledger.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/machine.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/plan_paths.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/render.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/snapshot.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/staging.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/src/tddcli/target_lint.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/conftest.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_advance_adoption.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_ancillary_files.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_artifact_regeneration.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_baseline_integrity.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_baseline_sanity.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_batch_collection.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_close_sweep_gates.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_concurrent_advance.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_config_and_staging.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_config_drift.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_contract.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_docs_command.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_doctor_attribution.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_doctor_blockers.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_end_to_end.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_evidence_extraction.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_example_plan.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_exec_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_executor_attribution.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_executor_notes.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_failure_clipping.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_fleet.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_gradle_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_heartbeat.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_id_normalisation.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_init_detection.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_late_baseline.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_named_leases_and_timeout.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_pin_cycles.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_plan_paths.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_progress.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_project_commands.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_project_env.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_pytest_xdist_group.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_python_env_managers.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_refactor_cycles.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_release_surface.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_run_claim.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_sensitivity_evidence.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_single_project_repo.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_snapshot_and_identity.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_stub_hint.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_suite_overrides.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_target_lint.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_target_validation.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_timing_visibility.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_undeclared_close_gate.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_undeclared_dedup.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_vitest_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_worker_leases.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.1}/tests/test_xctest_adapter.py +0 -0
|
@@ -6,6 +6,24 @@ and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.1] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **cargo: a test target whose name differs from its file's stem** (#134): the adapter derived the
|
|
14
|
+
target from the `Running tests/<file>.rs` header's path, which is only the target's name under
|
|
15
|
+
cargo's default of one binary per `tests/*.rs`. A crate that sets `autotests = false` and declares
|
|
16
|
+
its own `[[test]]` — many files compiled into one binary named something else — was unreachable:
|
|
17
|
+
collection ran `--test <stem>` and cargo answered ``no test target named `<stem>` in default-run
|
|
18
|
+
packages``, so every cycle target came back `not_collected`, whatever id the plan declared. The
|
|
19
|
+
target is now read from the artifact the header names (`…/deps/bridge_tests-<hash>`), which is
|
|
20
|
+
also the only thing distinguishing two crates in a workspace whose test files are both
|
|
21
|
+
`tests/main.rs`. The crate manifests supply the mapping back: target → the file it is built from
|
|
22
|
+
for `target_path` and the collection file set, and the declared spelling for `--test`, since cargo
|
|
23
|
+
writes a target's `-` as `_` in the artifact. Manifests are located by walking up from the declared
|
|
24
|
+
`test_paths`, not from `_test_files()`, which does not expand a wildcard directory pattern such as
|
|
25
|
+
`crates/*/tests/`. Under the default layout target and stem still agree and nothing changes.
|
|
26
|
+
|
|
9
27
|
## [0.12.0] - 2026-09-17
|
|
10
28
|
|
|
11
29
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tdd-cli
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.1
|
|
4
4
|
Summary: Ledger-backed TDD process controller for autonomous coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/geuben/tdd-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/geuben/tdd-cli
|
|
@@ -8,6 +8,16 @@ binary name for a file under `tests/` (`tests/roundtrip.rs` → `roundtrip`); th
|
|
|
8
8
|
lib::layout::tests::wraps_short
|
|
9
9
|
roundtrip::renders_cover_only
|
|
10
10
|
|
|
11
|
+
The target is the *binary* name, which is only the file stem under cargo's
|
|
12
|
+
default of one binary per `tests/*.rs`. A crate that sets `autotests = false`
|
|
13
|
+
and declares its own `[[test]]` compiles many files into one binary named
|
|
14
|
+
something else entirely — `tests/main.rs` with a `mod` per file, built as
|
|
15
|
+
`bridge_tests` — and then ids read `bridge_tests::adapter_config::parses`, the
|
|
16
|
+
file is `tests/main.rs`, and `--test bridge_tests` is what runs it. The manifest
|
|
17
|
+
is the only thing relating the two, so it is read; the `Running` header's
|
|
18
|
+
artifact (`…/deps/bridge_tests-<hash>`) is what names the target per block,
|
|
19
|
+
since the header's path is `tests/main.rs` in every crate of a workspace.
|
|
20
|
+
|
|
11
21
|
A targeted run composes to cargo's own selectors without translation —
|
|
12
22
|
`cargo test --lib -- --exact layout::tests::wraps_short` or
|
|
13
23
|
`cargo test --test roundtrip -- --exact renders_cover_only` — and nothing else
|
|
@@ -49,6 +59,7 @@ from __future__ import annotations
|
|
|
49
59
|
import re
|
|
50
60
|
import shlex
|
|
51
61
|
import time
|
|
62
|
+
import tomllib
|
|
52
63
|
from pathlib import Path
|
|
53
64
|
|
|
54
65
|
from .base import (
|
|
@@ -63,7 +74,14 @@ from .base import (
|
|
|
63
74
|
|
|
64
75
|
# ` Running unittests src/lib.rs (target/debug/deps/x-abc)` → target "lib"
|
|
65
76
|
# ` Running tests/roundtrip.rs (target/debug/deps/roundtrip-abc)` → "roundtrip"
|
|
66
|
-
|
|
77
|
+
# The artifact in parentheses is captured too: it is the only part of the header
|
|
78
|
+
# that names the *target*, which a crate may name differently from its file
|
|
79
|
+
# (`tests/main.rs` compiled as `bridge_tests`), and the only part that tells two
|
|
80
|
+
# crates' identically-pathed test files apart in a workspace.
|
|
81
|
+
_RUNNING_RE = re.compile(r"^\s*Running (?:unittests (\S+)|(\S+)) \(([^)]*)\)", re.MULTILINE)
|
|
82
|
+
# `bridge_tests-541f4f38a741dff0` → `bridge_tests` (cargo abbreviates the hash
|
|
83
|
+
# in some outputs, so its length is not pinned)
|
|
84
|
+
_ARTIFACT_HASH_RE = re.compile(r"-[0-9a-f]+$")
|
|
67
85
|
_DOCTEST_RE = re.compile(r"^\s*Doc-tests ", re.MULTILINE)
|
|
68
86
|
# `test layout::tests::wraps_short ... ok` / `... FAILED` / `... ignored`
|
|
69
87
|
_TEST_LINE_RE = re.compile(r"^test (\S+) \.\.\. (ok|FAILED|ignored)\s*$", re.MULTILINE)
|
|
@@ -76,18 +94,36 @@ _COULD_NOT_COMPILE_RE = re.compile(r"could not compile")
|
|
|
76
94
|
_NO_LIB_RE = re.compile(r"no library targets found")
|
|
77
95
|
|
|
78
96
|
|
|
97
|
+
def _artifact_target(artifact: str) -> str | None:
|
|
98
|
+
"""Target name from a `target/debug/deps/<name>-<hash>` artifact path.
|
|
99
|
+
|
|
100
|
+
cargo writes `-` in a target name as `_` here, so the result is the mangled
|
|
101
|
+
spelling; `CargoAdapter._demangle_target` maps it back to the declared one.
|
|
102
|
+
"""
|
|
103
|
+
stem = Path(artifact).name
|
|
104
|
+
stripped = _ARTIFACT_HASH_RE.sub("", stem)
|
|
105
|
+
return stripped or None
|
|
106
|
+
|
|
107
|
+
|
|
79
108
|
def _target_of_header(match: re.Match) -> str | None:
|
|
80
|
-
unit, path = match.group(1), match.group(2)
|
|
109
|
+
unit, path, artifact = match.group(1), match.group(2), match.group(3)
|
|
81
110
|
if unit is not None:
|
|
82
111
|
return "lib"
|
|
83
112
|
if path.startswith("tests/") and path.endswith(".rs"):
|
|
84
|
-
|
|
113
|
+
# The artifact names the target; the path only names the file it was
|
|
114
|
+
# built from, and those differ whenever a crate declares `[[test]]`
|
|
115
|
+
# with a `name` of its own.
|
|
116
|
+
return _artifact_target(artifact) or Path(path).stem
|
|
85
117
|
return None # examples/benches: not test targets
|
|
86
118
|
|
|
87
119
|
|
|
88
120
|
class CargoAdapter(Adapter):
|
|
89
121
|
name = "cargo"
|
|
90
122
|
|
|
123
|
+
def __init__(self, *args, **kwargs):
|
|
124
|
+
super().__init__(*args, **kwargs)
|
|
125
|
+
self._targets_cache: dict[str, str] | None = None
|
|
126
|
+
|
|
91
127
|
def stub_hint(self) -> str:
|
|
92
128
|
return (
|
|
93
129
|
"a compiling stub — a function whose body is `todo!()` — so the test"
|
|
@@ -99,7 +135,10 @@ class CargoAdapter(Adapter):
|
|
|
99
135
|
if "::" not in native:
|
|
100
136
|
return (
|
|
101
137
|
f"cargo target ids must be '<target>::<path>' (got {native!r}); expected"
|
|
102
|
-
" shape: lib::module::tests::name or <
|
|
138
|
+
" shape: lib::module::tests::name or <test-binary>::name — the"
|
|
139
|
+
" binary is the tests file's stem unless the crate declares"
|
|
140
|
+
" [[test]] name, and `cargo test --tests -- --list` prints the"
|
|
141
|
+
" spelling to use"
|
|
103
142
|
)
|
|
104
143
|
return None
|
|
105
144
|
|
|
@@ -107,7 +146,90 @@ class CargoAdapter(Adapter):
|
|
|
107
146
|
target, _, _ = native.partition("::")
|
|
108
147
|
if target == "lib":
|
|
109
148
|
return None
|
|
110
|
-
|
|
149
|
+
declared = self._declared_target(target)
|
|
150
|
+
path = self._manifest_targets().get(declared)
|
|
151
|
+
# No `[[test]]` entry: cargo's default, one binary per file, named for it.
|
|
152
|
+
return path or f"tests/{target}.rs"
|
|
153
|
+
|
|
154
|
+
# ------------------------------------------------------------------
|
|
155
|
+
# Target names vs. their files
|
|
156
|
+
#
|
|
157
|
+
# By default a target is named after its file (`tests/roundtrip.rs` →
|
|
158
|
+
# `roundtrip`), and the two are interchangeable. A crate that sets
|
|
159
|
+
# `autotests = false` and declares its own `[[test]]` breaks that: several
|
|
160
|
+
# files compile into one binary whose `name` is unrelated to any of them
|
|
161
|
+
# (`tests/main.rs` → `bridge_tests`). Only the manifest relates the two.
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def _manifest_targets(self) -> dict[str, str]:
|
|
165
|
+
"""`{declared [[test]] name: path relative to the project root}`.
|
|
166
|
+
|
|
167
|
+
Read from the manifest owning each file under `test_paths`, so a
|
|
168
|
+
workspace contributes one entry per crate that declares a test target.
|
|
169
|
+
"""
|
|
170
|
+
if self._targets_cache is None:
|
|
171
|
+
targets: dict[str, str] = {}
|
|
172
|
+
for manifest in self._manifests():
|
|
173
|
+
try:
|
|
174
|
+
raw = tomllib.loads(manifest.read_text())
|
|
175
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
176
|
+
continue
|
|
177
|
+
for entry in raw.get("test", []) or []:
|
|
178
|
+
name, rel = entry.get("name"), entry.get("path")
|
|
179
|
+
if not name or not rel:
|
|
180
|
+
continue
|
|
181
|
+
try:
|
|
182
|
+
full = (manifest.parent / rel).resolve().relative_to(self.root.resolve())
|
|
183
|
+
except ValueError:
|
|
184
|
+
continue
|
|
185
|
+
targets[name] = str(full)
|
|
186
|
+
self._targets_cache = targets
|
|
187
|
+
return self._targets_cache
|
|
188
|
+
|
|
189
|
+
def _manifests(self) -> set[Path]:
|
|
190
|
+
"""The nearest Cargo.toml above each declared test directory.
|
|
191
|
+
|
|
192
|
+
Walked from `test_paths` rather than `_test_files()`, which does not
|
|
193
|
+
expand a wildcard directory pattern (`crates/*/tests/`) — the very shape
|
|
194
|
+
a workspace of crates uses.
|
|
195
|
+
"""
|
|
196
|
+
found: set[Path] = set()
|
|
197
|
+
root = self.root.resolve()
|
|
198
|
+
for pattern in self.project.test_patterns or []:
|
|
199
|
+
for match in self.root.glob(pattern.rstrip("/") or "."):
|
|
200
|
+
start = match.resolve()
|
|
201
|
+
# Bounded by construction: a glob of the root cannot escape it,
|
|
202
|
+
# so the ascent always reaches root and stops there.
|
|
203
|
+
for here in [start, *start.parents]:
|
|
204
|
+
candidate = here / "Cargo.toml"
|
|
205
|
+
if candidate.is_file():
|
|
206
|
+
found.add(candidate)
|
|
207
|
+
break
|
|
208
|
+
if here == root:
|
|
209
|
+
break
|
|
210
|
+
return found
|
|
211
|
+
|
|
212
|
+
def _declared_target(self, target: str) -> str:
|
|
213
|
+
"""The `[[test]] name` behind a collected target name.
|
|
214
|
+
|
|
215
|
+
cargo writes a target's `-` as `_` in the artifact filename the id is
|
|
216
|
+
read from, so `my-tests` is collected as `my_tests`; `--test` wants the
|
|
217
|
+
declared spelling back.
|
|
218
|
+
"""
|
|
219
|
+
names = self._manifest_targets()
|
|
220
|
+
if target in names:
|
|
221
|
+
return target
|
|
222
|
+
for declared in names:
|
|
223
|
+
if declared.replace("-", "_") == target:
|
|
224
|
+
return declared
|
|
225
|
+
return target
|
|
226
|
+
|
|
227
|
+
def _target_of_file(self, rel: str) -> str:
|
|
228
|
+
"""The target a test file belongs to; its stem under cargo's default."""
|
|
229
|
+
for declared, path in self._manifest_targets().items():
|
|
230
|
+
if path == rel:
|
|
231
|
+
return declared
|
|
232
|
+
return Path(rel).stem
|
|
111
233
|
|
|
112
234
|
# ------------------------------------------------------------------
|
|
113
235
|
# Commands
|
|
@@ -116,10 +238,12 @@ class CargoAdapter(Adapter):
|
|
|
116
238
|
def _test_cmd(self) -> str:
|
|
117
239
|
return self.project.test_command or "cargo test --tests"
|
|
118
240
|
|
|
119
|
-
|
|
120
|
-
def _selector(native_id: str) -> str:
|
|
241
|
+
def _selector(self, native_id: str) -> str:
|
|
121
242
|
target, _, path = native_id.partition("::")
|
|
122
|
-
|
|
243
|
+
if target == "lib":
|
|
244
|
+
scope = "--lib"
|
|
245
|
+
else:
|
|
246
|
+
scope = f"--test {shlex.quote(self._declared_target(target))}"
|
|
123
247
|
return f"{scope} -- --exact {shlex.quote(path)}"
|
|
124
248
|
|
|
125
249
|
def _targeted_cmd(self, native_id: str) -> str:
|
|
@@ -154,18 +278,19 @@ class CargoAdapter(Adapter):
|
|
|
154
278
|
if code != 0 or _COULD_NOT_COMPILE_RE.search(out):
|
|
155
279
|
return None
|
|
156
280
|
tests = self._parse_list(out)
|
|
157
|
-
files = {
|
|
281
|
+
files = {self.target_path(t) for t in tests if not t.startswith("lib::")} - {None}
|
|
158
282
|
return {self.qualify(t) for t in tests}, files
|
|
159
283
|
|
|
160
284
|
def _collect_per_file(self, rels, result):
|
|
161
285
|
for rel in sorted(rels):
|
|
162
|
-
|
|
163
|
-
cmd = f"{self._targeted_prefix()} --test {shlex.quote(
|
|
286
|
+
declared = self._target_of_file(rel)
|
|
287
|
+
cmd = f"{self._targeted_prefix()} --test {shlex.quote(declared)} -- --list 2>&1"
|
|
164
288
|
code, out, _err = self._run_suite(cmd, self._suite_env(None))
|
|
165
289
|
if code != 0 or _COULD_NOT_COMPILE_RE.search(out):
|
|
166
290
|
result.failed_files[rel] = clip_failure(self._errors(out))
|
|
167
291
|
continue
|
|
168
|
-
|
|
292
|
+
default = declared.replace("-", "_")
|
|
293
|
+
result.tests |= {self.qualify(t) for t in self._parse_list(out, default=default)}
|
|
169
294
|
return result
|
|
170
295
|
|
|
171
296
|
def _targeted_prefix(self) -> str:
|
|
@@ -13,7 +13,7 @@ from unittest.mock import patch
|
|
|
13
13
|
|
|
14
14
|
from tddcli import config as config_mod
|
|
15
15
|
from tddcli.adapters import available
|
|
16
|
-
from tddcli.adapters.base import FAILED, NOT_COLLECTED, NOT_FOUND, PASSED
|
|
16
|
+
from tddcli.adapters.base import FAILED, NOT_COLLECTED, NOT_FOUND, PASSED, Collection
|
|
17
17
|
from tddcli.adapters.cargo_adapter import CargoAdapter
|
|
18
18
|
|
|
19
19
|
TOML = """
|
|
@@ -395,3 +395,190 @@ def test_every_invocation_carries_the_project_env(tmp_path):
|
|
|
395
395
|
for call in rs.call_args_list:
|
|
396
396
|
env = call.args[1] if len(call.args) > 1 else call.kwargs.get("extra_env")
|
|
397
397
|
assert env == {"CARGO_HOME": "/opt/cargo"}, call
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
# A crate whose test binary is not named after its file
|
|
402
|
+
#
|
|
403
|
+
# `autotests = false` plus a `[[test]]` entry compiles every file under tests/
|
|
404
|
+
# into one binary with a name of its own. The target is then unrelated to the
|
|
405
|
+
# file it was built from, and in a workspace every crate's header reads
|
|
406
|
+
# `Running tests/main.rs` — only the artifact tells them apart.
|
|
407
|
+
# ---------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
CONSOLIDATED_LIST = """
|
|
410
|
+
Running tests/main.rs (/work/target/debug/deps/bridge_tests-541f4f38a741dff0)
|
|
411
|
+
|
|
412
|
+
adapter_config::poll_config_parses_targets_and_maps: test
|
|
413
|
+
adapter_webhook_config::webhook_table_parses_listen_token_and_maps: test
|
|
414
|
+
|
|
415
|
+
2 tests, 0 benchmarks
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
WORKSPACE_LIST = """
|
|
419
|
+
Running tests/main.rs (/work/target/debug/deps/bridge_tests-541f4f38a741dff0)
|
|
420
|
+
|
|
421
|
+
adapter_config::poll_config_parses_targets_and_maps: test
|
|
422
|
+
|
|
423
|
+
Running tests/main.rs (/work/target/debug/deps/core_tests-9f1e2d3c4b5a6879)
|
|
424
|
+
|
|
425
|
+
attention_clear::clear_removes_the_alert: test
|
|
426
|
+
|
|
427
|
+
2 tests, 0 benchmarks
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def make_consolidated_adapter(tmp_path: Path) -> CargoAdapter:
|
|
432
|
+
"""A workspace of two crates, each compiling tests/main.rs under its own name."""
|
|
433
|
+
(tmp_path / "tdd.toml").write_text(
|
|
434
|
+
"\n".join(
|
|
435
|
+
[
|
|
436
|
+
"[project.ws]",
|
|
437
|
+
'root = "."',
|
|
438
|
+
'adapter = "cargo"',
|
|
439
|
+
'test_paths = ["crates/*/tests/"]',
|
|
440
|
+
"",
|
|
441
|
+
]
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
for crate, target in (("dd-bridge", "bridge_tests"), ("dd-core", "core_tests")):
|
|
445
|
+
tests = tmp_path / "crates" / crate / "tests"
|
|
446
|
+
tests.mkdir(parents=True)
|
|
447
|
+
(tests / "main.rs").write_text("mod adapter_config;\n")
|
|
448
|
+
(tmp_path / "crates" / crate / "Cargo.toml").write_text(
|
|
449
|
+
"\n".join(
|
|
450
|
+
[
|
|
451
|
+
"[package]",
|
|
452
|
+
f'name = "{crate}"',
|
|
453
|
+
"autotests = false",
|
|
454
|
+
"",
|
|
455
|
+
"[[test]]",
|
|
456
|
+
f'name = "{target}"',
|
|
457
|
+
'path = "tests/main.rs"',
|
|
458
|
+
"",
|
|
459
|
+
]
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
cfg = config_mod.load(tmp_path)
|
|
463
|
+
return CargoAdapter(cfg.project("ws"), tmp_path)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_ids_use_the_binary_name_not_the_file_stem(tmp_path):
|
|
467
|
+
a = make_consolidated_adapter(tmp_path)
|
|
468
|
+
assert a._parse_list(CONSOLIDATED_LIST) == {
|
|
469
|
+
"bridge_tests::adapter_config::poll_config_parses_targets_and_maps",
|
|
470
|
+
"bridge_tests::adapter_webhook_config::webhook_table_parses_listen_token_and_maps",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def test_identically_pathed_files_are_told_apart_by_their_artifact(tmp_path):
|
|
475
|
+
a = make_consolidated_adapter(tmp_path)
|
|
476
|
+
assert a._parse_list(WORKSPACE_LIST) == {
|
|
477
|
+
"bridge_tests::adapter_config::poll_config_parses_targets_and_maps",
|
|
478
|
+
"core_tests::attention_clear::clear_removes_the_alert",
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def test_targeted_command_scopes_to_the_declared_binary(tmp_path):
|
|
483
|
+
a = make_consolidated_adapter(tmp_path)
|
|
484
|
+
assert a._targeted_cmd("bridge_tests::adapter_config::parses") == (
|
|
485
|
+
"cargo test --test bridge_tests -- --exact adapter_config::parses"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_target_path_maps_a_declared_binary_to_the_file_it_is_built_from(tmp_path):
|
|
490
|
+
a = make_consolidated_adapter(tmp_path)
|
|
491
|
+
assert a.target_path("bridge_tests::adapter_config::parses") == (
|
|
492
|
+
"crates/dd-bridge/tests/main.rs"
|
|
493
|
+
)
|
|
494
|
+
assert a.target_path("core_tests::attention_clear::clears") == ("crates/dd-core/tests/main.rs")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def test_per_file_collection_scopes_to_the_files_binary(tmp_path):
|
|
498
|
+
a = make_consolidated_adapter(tmp_path)
|
|
499
|
+
seen: list[str] = []
|
|
500
|
+
|
|
501
|
+
def record(cmd, env):
|
|
502
|
+
seen.append(cmd)
|
|
503
|
+
return 0, CONSOLIDATED_LIST, ""
|
|
504
|
+
|
|
505
|
+
with patch.object(CargoAdapter, "_run_suite", side_effect=record):
|
|
506
|
+
a._collect_per_file({"crates/dd-bridge/tests/main.rs"}, Collection())
|
|
507
|
+
assert "--test bridge_tests" in seen[0]
|
|
508
|
+
assert "--test main" not in seen[0]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def test_a_dashed_binary_name_is_run_under_its_declared_spelling(tmp_path):
|
|
512
|
+
"""cargo writes `-` as `_` in the artifact, but `--test` wants the manifest's spelling."""
|
|
513
|
+
(tmp_path / "tdd.toml").write_text(
|
|
514
|
+
'[project.ws]\nroot = "."\nadapter = "cargo"\ntest_paths = ["tests/"]\n'
|
|
515
|
+
)
|
|
516
|
+
(tmp_path / "tests").mkdir()
|
|
517
|
+
(tmp_path / "tests" / "main.rs").write_text("mod a;\n")
|
|
518
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
519
|
+
'[package]\nname = "w"\nautotests = false\n\n'
|
|
520
|
+
'[[test]]\nname = "my-tests"\npath = "tests/main.rs"\n'
|
|
521
|
+
)
|
|
522
|
+
cfg = config_mod.load(tmp_path)
|
|
523
|
+
a = CargoAdapter(cfg.project("ws"), tmp_path)
|
|
524
|
+
assert a._targeted_cmd("my_tests::a::works") == (
|
|
525
|
+
"cargo test --test my-tests -- --exact a::works"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def test_collection_reports_the_file_each_target_was_built_from(tmp_path):
|
|
530
|
+
"""The set of covered files is the manifest's, not `tests/<target>.rs`."""
|
|
531
|
+
a = make_consolidated_adapter(tmp_path)
|
|
532
|
+
with patch.object(CargoAdapter, "_run_suite", return_value=(0, WORKSPACE_LIST, "")):
|
|
533
|
+
tests, files = a._collect_batch("cargo test --tests -- --list", {})
|
|
534
|
+
assert files == {"crates/dd-bridge/tests/main.rs", "crates/dd-core/tests/main.rs"}
|
|
535
|
+
assert "ws::bridge_tests::adapter_config::poll_config_parses_targets_and_maps" in tests
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def test_a_malformed_artifact_falls_back_to_the_file_stem(tmp_path):
|
|
539
|
+
"""Nothing usable in the parentheses: the file stem is still cargo's default."""
|
|
540
|
+
a = make_adapter(tmp_path)
|
|
541
|
+
output = (
|
|
542
|
+
" Running tests/roundtrip.rs (/work/target/debug/deps/-1d2c3b4a)\n"
|
|
543
|
+
"\nrenders_cover_only: test\n"
|
|
544
|
+
)
|
|
545
|
+
assert a._parse_list(output) == {"roundtrip::renders_cover_only"}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def test_a_test_entry_without_a_path_resolves_through_cargos_default(tmp_path):
|
|
549
|
+
"""`[[test]] name = "x"` with no `path` means `tests/x.rs`; no mapping needed."""
|
|
550
|
+
(tmp_path / "tdd.toml").write_text(
|
|
551
|
+
'[project.ws]\nroot = "."\nadapter = "cargo"\ntest_paths = ["tests/"]\n'
|
|
552
|
+
)
|
|
553
|
+
(tmp_path / "tests").mkdir()
|
|
554
|
+
(tmp_path / "tests" / "roundtrip.rs").write_text("#[test] fn t() {}\n")
|
|
555
|
+
(tmp_path / "Cargo.toml").write_text('[package]\nname = "w"\n\n[[test]]\nname = "roundtrip"\n')
|
|
556
|
+
cfg = config_mod.load(tmp_path)
|
|
557
|
+
a = CargoAdapter(cfg.project("ws"), tmp_path)
|
|
558
|
+
assert a._manifest_targets() == {}
|
|
559
|
+
assert a.target_path("roundtrip::t") == "tests/roundtrip.rs"
|
|
560
|
+
assert a._targeted_cmd("roundtrip::t") == ("cargo test --test roundtrip -- --exact t")
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def test_a_file_shaped_test_path_still_finds_the_manifest(tmp_path):
|
|
564
|
+
"""`test_paths` may name files (`tests/*.rs`), not only directories."""
|
|
565
|
+
(tmp_path / "tdd.toml").write_text(
|
|
566
|
+
'[project.ws]\nroot = "."\nadapter = "cargo"\ntest_paths = ["tests/*.rs"]\n'
|
|
567
|
+
)
|
|
568
|
+
(tmp_path / "tests").mkdir()
|
|
569
|
+
(tmp_path / "tests" / "main.rs").write_text("mod a;\n")
|
|
570
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
571
|
+
'[package]\nname = "w"\nautotests = false\n\n'
|
|
572
|
+
'[[test]]\nname = "w_tests"\npath = "tests/main.rs"\n'
|
|
573
|
+
)
|
|
574
|
+
cfg = config_mod.load(tmp_path)
|
|
575
|
+
a = CargoAdapter(cfg.project("ws"), tmp_path)
|
|
576
|
+
assert a._manifest_targets() == {"w_tests": "tests/main.rs"}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def test_a_crate_with_no_manifest_keeps_cargos_default_mapping(tmp_path):
|
|
580
|
+
"""No Cargo.toml above the tests dir: the walk stops at the root, nothing is claimed."""
|
|
581
|
+
a = make_adapter(tmp_path) # kernel/tests/roundtrip.rs, no Cargo.toml anywhere
|
|
582
|
+
assert a._manifest_targets() == {}
|
|
583
|
+
assert a.target_path("roundtrip::renders_cover_only") == "tests/roundtrip.rs"
|
|
584
|
+
assert a._target_of_file("tests/roundtrip.rs") == "roundtrip"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|