tdd-cli 0.12.0__tar.gz → 0.12.2__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.2}/CHANGELOG.md +32 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/PKG-INFO +1 -1
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/__init__.py +1 -1
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/adapters/cargo_adapter.py +144 -15
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/config.py +19 -2
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_cargo_adapter.py +195 -1
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_config_and_staging.py +39 -1
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/.gitignore +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/LICENSE +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/README.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/SECURITY.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/docs/PRD.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/docs/harness-integration.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/examples/claude-code-hooks/README.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/examples/claude-code-hooks/bash_hook.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/examples/claude-code-hooks/stop_hook.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/examples/plan.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/examples/skills/tdd-drive/README.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/examples/skills/tdd-drive/SKILL.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/examples/skills/tdd-handoff/README.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/examples/skills/tdd-handoff/SKILL.md +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/pyproject.toml +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/adapters/__init__.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/adapters/base.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/adapters/exec_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/adapters/gradle_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/adapters/pytest_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/adapters/vitest_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/adapters/xctest_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/advance.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/cli.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/contract.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/docs.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/envelope.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/fleet.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/gitutil.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/identity.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/leases.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/ledger.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/machine.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/plan_paths.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/render.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/snapshot.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/staging.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/src/tddcli/target_lint.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/conftest.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_advance_adoption.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_ancillary_files.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_artifact_regeneration.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_baseline_integrity.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_baseline_sanity.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_batch_collection.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_close_sweep_gates.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_concurrent_advance.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_config_drift.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_contract.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_docs_command.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_doctor_attribution.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_doctor_blockers.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_end_to_end.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_evidence_extraction.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_example_plan.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_exec_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_executor_attribution.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_executor_notes.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_failure_clipping.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_fleet.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_gradle_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_heartbeat.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_id_normalisation.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_init_detection.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_late_baseline.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_named_leases_and_timeout.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_pin_cycles.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_plan_paths.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_progress.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_project_commands.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_project_env.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_pytest_xdist_group.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_python_env_managers.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_refactor_cycles.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_release_surface.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_run_claim.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_sensitivity_evidence.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_single_project_repo.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_snapshot_and_identity.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_stub_hint.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_suite_overrides.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_target_lint.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_target_validation.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_timing_visibility.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_undeclared_close_gate.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_undeclared_dedup.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_vitest_adapter.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_worker_leases.py +0 -0
- {tdd_cli-0.12.0 → tdd_cli-0.12.2}/tests/test_xctest_adapter.py +0 -0
|
@@ -6,6 +6,38 @@ and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.2] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **A wildcard directory in `test_paths` matched nothing** (#136): a pattern ending in `/` was
|
|
14
|
+
compared as a literal prefix by `Project.is_test_file` and joined as a literal path by the cargo
|
|
15
|
+
adapter's `_test_files`, so `crates/*/tests/` — how a workspace declares one pattern instead of
|
|
16
|
+
one per crate — matched no file at all. Every test file in such a project classified as
|
|
17
|
+
implementation, so a RED commit touching only tests was recorded as `implementation_during_red`,
|
|
18
|
+
reporting the agent for writing production code it had not written; `_test_files` returned an
|
|
19
|
+
empty list, leaving the unaccounted-files check with nothing to compare. A directory pattern is
|
|
20
|
+
now matched per path segment (`crates/*/tests/` covers `crates/x/tests/main.rs`, not
|
|
21
|
+
`crates/x/src/tests/main.rs`), and the adapter globs it.
|
|
22
|
+
|
|
23
|
+
## [0.12.1] - 2026-09-17
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **cargo: a test target whose name differs from its file's stem** (#134): the adapter derived the
|
|
28
|
+
target from the `Running tests/<file>.rs` header's path, which is only the target's name under
|
|
29
|
+
cargo's default of one binary per `tests/*.rs`. A crate that sets `autotests = false` and declares
|
|
30
|
+
its own `[[test]]` — many files compiled into one binary named something else — was unreachable:
|
|
31
|
+
collection ran `--test <stem>` and cargo answered ``no test target named `<stem>` in default-run
|
|
32
|
+
packages``, so every cycle target came back `not_collected`, whatever id the plan declared. The
|
|
33
|
+
target is now read from the artifact the header names (`…/deps/bridge_tests-<hash>`), which is
|
|
34
|
+
also the only thing distinguishing two crates in a workspace whose test files are both
|
|
35
|
+
`tests/main.rs`. The crate manifests supply the mapping back: target → the file it is built from
|
|
36
|
+
for `target_path` and the collection file set, and the declared spelling for `--test`, since cargo
|
|
37
|
+
writes a target's `-` as `_` in the artifact. Manifests are located by walking up from the declared
|
|
38
|
+
`test_paths`, not from `_test_files()`, which does not expand a wildcard directory pattern such as
|
|
39
|
+
`crates/*/tests/`. Under the default layout target and stem still agree and nothing changes.
|
|
40
|
+
|
|
9
41
|
## [0.12.0] - 2026-09-17
|
|
10
42
|
|
|
11
43
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tdd-cli
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.2
|
|
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:
|
|
@@ -135,9 +259,13 @@ class CargoAdapter(Adapter):
|
|
|
135
259
|
found: set[Path] = set()
|
|
136
260
|
for pattern in self.project.test_patterns or []:
|
|
137
261
|
if pattern.endswith("/"):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
262
|
+
# `self.root / pattern` is a literal path, so a wildcard
|
|
263
|
+
# directory pattern (`crates/*/tests/`) never resolves; glob it.
|
|
264
|
+
# A trailing slash is accepted by glob on every supported
|
|
265
|
+
# Python (3.11–3.14), so the pattern goes through as declared.
|
|
266
|
+
for base in self.root.glob(pattern):
|
|
267
|
+
if base.is_dir():
|
|
268
|
+
found.update(p for p in base.glob("*.rs") if p.is_file())
|
|
141
269
|
else:
|
|
142
270
|
found.update(p for p in self.root.glob(pattern) if p.is_file())
|
|
143
271
|
return sorted(found)
|
|
@@ -154,18 +282,19 @@ class CargoAdapter(Adapter):
|
|
|
154
282
|
if code != 0 or _COULD_NOT_COMPILE_RE.search(out):
|
|
155
283
|
return None
|
|
156
284
|
tests = self._parse_list(out)
|
|
157
|
-
files = {
|
|
285
|
+
files = {self.target_path(t) for t in tests if not t.startswith("lib::")} - {None}
|
|
158
286
|
return {self.qualify(t) for t in tests}, files
|
|
159
287
|
|
|
160
288
|
def _collect_per_file(self, rels, result):
|
|
161
289
|
for rel in sorted(rels):
|
|
162
|
-
|
|
163
|
-
cmd = f"{self._targeted_prefix()} --test {shlex.quote(
|
|
290
|
+
declared = self._target_of_file(rel)
|
|
291
|
+
cmd = f"{self._targeted_prefix()} --test {shlex.quote(declared)} -- --list 2>&1"
|
|
164
292
|
code, out, _err = self._run_suite(cmd, self._suite_env(None))
|
|
165
293
|
if code != 0 or _COULD_NOT_COMPILE_RE.search(out):
|
|
166
294
|
result.failed_files[rel] = clip_failure(self._errors(out))
|
|
167
295
|
continue
|
|
168
|
-
|
|
296
|
+
default = declared.replace("-", "_")
|
|
297
|
+
result.tests |= {self.qualify(t) for t in self._parse_list(out, default=default)}
|
|
169
298
|
return result
|
|
170
299
|
|
|
171
300
|
def _targeted_prefix(self) -> str:
|
|
@@ -73,6 +73,20 @@ class Override:
|
|
|
73
73
|
timeout: int | None = None
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _dir_pattern_matches(pattern: str, inner: str) -> bool:
|
|
77
|
+
"""True when `inner` sits under the directory `pattern` describes.
|
|
78
|
+
|
|
79
|
+
`pattern` ends in `/` and may contain wildcards; matching is per path
|
|
80
|
+
segment, so `crates/*/tests/` covers `crates/dd-bridge/tests/main.rs` but
|
|
81
|
+
not `crates/dd-bridge/src/main.rs`.
|
|
82
|
+
"""
|
|
83
|
+
segments = pattern.rstrip("/").split("/")
|
|
84
|
+
parts = inner.split("/")
|
|
85
|
+
if len(parts) <= len(segments):
|
|
86
|
+
return False
|
|
87
|
+
return all(fnmatch(part, seg) for part, seg in zip(parts, segments))
|
|
88
|
+
|
|
89
|
+
|
|
76
90
|
@dataclass
|
|
77
91
|
class Project:
|
|
78
92
|
name: str
|
|
@@ -141,11 +155,14 @@ class Project:
|
|
|
141
155
|
inner = self.relative_to_root(rel_path)
|
|
142
156
|
for pattern in self.test_patterns:
|
|
143
157
|
if pattern.endswith("/"):
|
|
144
|
-
|
|
158
|
+
# A directory pattern may carry wildcards (`crates/*/tests/`),
|
|
159
|
+
# so it is matched as a glob against the leading path segments,
|
|
160
|
+
# not compared as a literal prefix.
|
|
161
|
+
if _dir_pattern_matches(pattern, inner):
|
|
145
162
|
return True
|
|
146
163
|
elif fnmatch(inner, pattern) or fnmatch(rel_path, pattern):
|
|
147
164
|
return True
|
|
148
|
-
elif
|
|
165
|
+
elif _dir_pattern_matches(pattern.rstrip("/") + "/", inner):
|
|
149
166
|
return True
|
|
150
167
|
return False
|
|
151
168
|
|
|
@@ -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,197 @@ 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"
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def test_test_files_expands_a_wildcard_directory_pattern(tmp_path):
|
|
588
|
+
"""`self.root / "crates/*/tests/"` is a literal path and never resolves."""
|
|
589
|
+
a = make_consolidated_adapter(tmp_path)
|
|
590
|
+
rels = sorted(str(p.relative_to(tmp_path)) for p in a._test_files())
|
|
591
|
+
assert rels == ["crates/dd-bridge/tests/main.rs", "crates/dd-core/tests/main.rs"]
|
|
@@ -21,6 +21,11 @@ adapter = "vitest"
|
|
|
21
21
|
test_paths = ["e2e/"]
|
|
22
22
|
in_close_sweep = false
|
|
23
23
|
|
|
24
|
+
[project.workspace]
|
|
25
|
+
root = "ws"
|
|
26
|
+
adapter = "cargo"
|
|
27
|
+
test_paths = ["crates/*/tests/"]
|
|
28
|
+
|
|
24
29
|
[artifact.openapi]
|
|
25
30
|
path = "schema/openapi.json"
|
|
26
31
|
produced_by = "backend"
|
|
@@ -43,7 +48,7 @@ def cfg(tmp_path):
|
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
def test_projects_and_roots_are_declared_not_scanned(cfg):
|
|
46
|
-
assert sorted(cfg.projects) == ["backend", "e2e", "frontend"]
|
|
51
|
+
assert sorted(cfg.projects) == ["backend", "e2e", "frontend", "workspace"]
|
|
47
52
|
assert cfg.project("backend").root == "backend"
|
|
48
53
|
|
|
49
54
|
|
|
@@ -320,3 +325,36 @@ def test_health_command_parses_onto_project(tmp_path):
|
|
|
320
325
|
)
|
|
321
326
|
with pytest.raises(config_mod.ConfigError):
|
|
322
327
|
config_mod.load(tmp_path)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_a_wildcard_directory_test_path_matches_per_segment(cfg):
|
|
331
|
+
"""`crates/*/tests/` is a glob, not a literal prefix.
|
|
332
|
+
|
|
333
|
+
A workspace declares its test directories with one pattern rather than one
|
|
334
|
+
per crate. Comparing it as a literal string classifies every test file as
|
|
335
|
+
implementation, which reports the agent for writing production code during
|
|
336
|
+
RED when it only edited tests.
|
|
337
|
+
"""
|
|
338
|
+
ws = cfg.project("workspace")
|
|
339
|
+
assert ws.is_test_file("ws/crates/dd-bridge/tests/main.rs")
|
|
340
|
+
assert ws.is_test_file("ws/crates/dd-core/tests/attention_clear.rs")
|
|
341
|
+
# The wildcard spans exactly one segment, and the directory itself is not a file.
|
|
342
|
+
assert not ws.is_test_file("ws/crates/dd-bridge/src/adapter/config.rs")
|
|
343
|
+
assert not ws.is_test_file("ws/crates/dd-bridge/src/tests/inner.rs")
|
|
344
|
+
assert not ws.is_test_file("ws/crates/a/b/tests/deep.rs")
|
|
345
|
+
assert not ws.is_test_file("ws/crates/dd-bridge/tests")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_a_wildcard_test_path_classifies_edits_as_tests_not_implementation(cfg):
|
|
349
|
+
"""The staging consequence: RED may touch these without being reported."""
|
|
350
|
+
changed = {
|
|
351
|
+
"ws/crates/dd-bridge/tests/main.rs",
|
|
352
|
+
"ws/crates/dd-bridge/tests/adapter_config.rs",
|
|
353
|
+
"ws/crates/dd-bridge/src/adapter/config.rs",
|
|
354
|
+
}
|
|
355
|
+
out = staging.classify(cfg, changed, ["workspace"], None, set())
|
|
356
|
+
assert out.tests == [
|
|
357
|
+
"ws/crates/dd-bridge/tests/adapter_config.rs",
|
|
358
|
+
"ws/crates/dd-bridge/tests/main.rs",
|
|
359
|
+
]
|
|
360
|
+
assert out.implementation == ["ws/crates/dd-bridge/src/adapter/config.rs"]
|
|
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
|