testing-conventions 0.0.2__tar.gz → 0.0.4__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.
Files changed (51) hide show
  1. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/PKG-INFO +1 -1
  2. testing_conventions-0.0.4/rust/CHANGELOG.md +30 -0
  3. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/Cargo.lock +1 -1
  4. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/Cargo.toml +1 -1
  5. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/MIGRATIONS.md +19 -1
  6. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/src/lib.rs +33 -1
  7. testing_conventions-0.0.4/rust/src/location.rs +241 -0
  8. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/clean/pkg/helper.py +1 -0
  9. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/clean/pkg/helper_test.py +1 -0
  10. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/clean/widget.py +1 -0
  11. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/clean/widget_test.py +1 -0
  12. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/exempt/__init__.py +1 -0
  13. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/exempt/pkg/__init__.py +1 -0
  14. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/exempt/widget.py +1 -0
  15. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/exempt/widget_test.py +1 -0
  16. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/red/lonely.py +1 -0
  17. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/red/pkg/orphan.py +1 -0
  18. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/red/pkg/paired.py +1 -0
  19. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/red/pkg/paired_test.py +1 -0
  20. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/red/widget.py +1 -0
  21. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/red/widget_test.py +1 -0
  22. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/ambient.d.mts +1 -0
  23. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/button.test.tsx +1 -0
  24. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/button.tsx +1 -0
  25. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/legacy.cts +1 -0
  26. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/legacy.test.cts +1 -0
  27. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/pkg/helper.test.ts +1 -0
  28. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/pkg/helper.ts +1 -0
  29. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/service.mts +1 -0
  30. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/service.test.mts +1 -0
  31. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/types.d.ts +1 -0
  32. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/widget.test.ts +1 -0
  33. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/clean/widget.ts +1 -0
  34. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/red/bridge.cts +1 -0
  35. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/red/lonely.ts +1 -0
  36. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/red/pkg/orphan.ts +1 -0
  37. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/red/shapes.d.cts +1 -0
  38. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/red/shapes.d.ts +1 -0
  39. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/red/solo.mts +1 -0
  40. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/red/widget.test.ts +1 -0
  41. testing_conventions-0.0.4/rust/tests/fixtures/unit_location/typescript/red/widget.ts +1 -0
  42. testing_conventions-0.0.4/rust/tests/unit_location.rs +125 -0
  43. testing_conventions-0.0.2/rust/CHANGELOG.md +0 -22
  44. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/README.md +0 -0
  45. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/pyproject.toml +0 -0
  46. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/src/config.rs +0 -0
  47. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/src/main.rs +0 -0
  48. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/tests/config_loader.rs +0 -0
  49. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/tests/fixtures/malformed.toml +0 -0
  50. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/tests/fixtures/unknown_key.toml +0 -0
  51. {testing_conventions-0.0.2 → testing_conventions-0.0.4}/rust/tests/fixtures/valid.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testing-conventions
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Enforce testing conventions in libraries (Python, TypeScript, and Rust).
5
5
  Author-email: Kevin Scott <me@thekevinscott.com>
6
6
  License: MIT
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## Unreleased
7
+
8
+ ### Added
9
+
10
+ - `config` module — `load_config()` parses one TOML config file into the new
11
+ in-memory `Config` schema (per-language `coverage` thresholds under the
12
+ `[python]` / `[typescript]` / `[rust]` tables) and self-validates on load:
13
+ unknown keys and malformed TOML are rejected. The parsed config is not
14
+ consumed yet. (#12)
15
+ - `location` module — `missing_unit_tests(root, language)` walks a directory tree
16
+ and returns every source file with no colocated unit test (sorted), enforcing
17
+ the README's "Location & Naming" rule per `Language`:
18
+ - **Python** (#15) — `foo.py` → `foo_test.py`; `*_test.py` and `__init__.py` are exempt.
19
+ - **TypeScript** (#18) — `foo-bar.ts` → `foo-bar.test.ts` across `.ts`/`.tsx`/`.mts`/`.cts`; `*.test.{ts,tsx,mts,cts}` are tests, `*.d.ts`/`*.d.mts`/`*.d.cts` are ignored, nothing is exempt.
20
+ - `unit-location [--lang python|typescript] <PATH>` CLI subcommand — runs the check
21
+ over a directory and exits non-zero, printing each source file missing its
22
+ colocated test. `--lang` defaults to `python`. (#15, #18)
23
+
24
+ ### Changed
25
+
26
+ ### Deprecated
27
+
28
+ ### Removed
29
+
30
+ ### Fixed
@@ -226,7 +226,7 @@ dependencies = [
226
226
 
227
227
  [[package]]
228
228
  name = "testing-conventions"
229
- version = "0.0.2"
229
+ version = "0.0.4"
230
230
  dependencies = [
231
231
  "anyhow",
232
232
  "clap",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "testing-conventions"
3
- version = "0.0.2"
3
+ version = "0.0.4"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  authors = ["Kevin Scott <me@thekevinscott.com>"]
@@ -21,9 +21,19 @@ reads one TOML file into it and validates the config itself (the self-guard) —
21
21
  unknown keys and malformed TOML are rejected rather than silently accepted.
22
22
  Purely additive; nothing consumes the parsed config yet.
23
23
 
24
+ Also adds the first structural rule — unit-test location/naming — for two
25
+ languages. `missing_unit_tests(root, language)` walks a directory and returns
26
+ every source file with no colocated test, and `unit-location [--lang …] <PATH>`
27
+ runs that check and exits non-zero on any orphan. Python (#15): `foo.py` →
28
+ `foo_test.py`, `__init__.py` exempt. TypeScript (#18): `foo-bar.ts` →
29
+ `foo-bar.test.ts` (`.ts`/`.tsx`/`.mts`/`.cts`), `*.d.ts`/`*.d.mts`/`*.d.cts`
30
+ ignored. Purely additive.
31
+
24
32
  ### Required changes
25
33
 
26
- None. New, additive API: `testing_conventions::config::{Config, load_config}`.
34
+ None. New, additive API: `testing_conventions::config::{Config, load_config}`,
35
+ `testing_conventions::location::{missing_unit_tests, Language}`, and the
36
+ `unit-location [--lang python|typescript] <PATH>` CLI subcommand.
27
37
 
28
38
  ### Deprecations removed
29
39
 
@@ -41,3 +51,11 @@ cd packages/rust && cargo test --test config_loader
41
51
 
42
52
  Expected: the loader's integration tests pass — the canonical config loads into
43
53
  memory, and unknown-key, malformed, and missing-file configs are rejected.
54
+
55
+ ```
56
+ cd packages/rust && cargo test --test unit_location
57
+ ```
58
+
59
+ Expected: the location check's integration tests pass for both languages — each
60
+ clean fixture reports no orphans, each red fixture reports its missing twins, and
61
+ `unit-location` exits non-zero on the red fixtures.
@@ -1,4 +1,7 @@
1
1
  pub mod config;
2
+ pub mod location;
3
+
4
+ use std::path::{Path, PathBuf};
2
5
 
3
6
  use clap::{Parser, Subcommand};
4
7
 
@@ -18,6 +21,14 @@ pub struct Cli {
18
21
  enum Command {
19
22
  /// Check the repository against its testing-conventions config.
20
23
  Check,
24
+ /// Check that every source file has a colocated unit test.
25
+ UnitLocation {
26
+ /// Directory to scan recursively.
27
+ path: PathBuf,
28
+ /// Language convention to enforce.
29
+ #[arg(long, value_enum, default_value = "python")]
30
+ lang: location::Language,
31
+ },
21
32
  }
22
33
 
23
34
  pub fn run<I, T>(args: I) -> anyhow::Result<i32>
@@ -27,9 +38,30 @@ where
27
38
  {
28
39
  let cli = Cli::try_parse_from(args)?;
29
40
  match cli.command {
30
- // Rules aren't implemented yet; the scaffold just proves the wiring.
41
+ // The config-driven `check` umbrella isn't wired yet; the scaffold
42
+ // proves the wiring while individual rules land as their own subcommand.
31
43
  Some(Command::Check) | None => Ok(0),
44
+ Some(Command::UnitLocation { path, lang }) => run_unit_location(&path, lang),
45
+ }
46
+ }
47
+
48
+ /// Run the unit-test location check over `root` for `language`, reporting orphans.
49
+ ///
50
+ /// Returns `0` when every source file has its colocated unit test; otherwise
51
+ /// prints each orphan to stderr and returns `1`.
52
+ fn run_unit_location(root: &Path, language: location::Language) -> anyhow::Result<i32> {
53
+ let orphans = location::missing_unit_tests(root, language)?;
54
+ if orphans.is_empty() {
55
+ return Ok(0);
56
+ }
57
+ for orphan in &orphans {
58
+ eprintln!("missing colocated unit test: {}", orphan.display());
32
59
  }
60
+ eprintln!(
61
+ "error: {} source file(s) missing a colocated unit test",
62
+ orphans.len()
63
+ );
64
+ Ok(1)
33
65
  }
34
66
 
35
67
  #[cfg(test)]
@@ -0,0 +1,241 @@
1
+ //! Unit-test location/naming check (Python — issue #15; TypeScript — issue #18).
2
+ //!
3
+ //! Convention (README "Location & Naming"; `internals/*/testing.md`): a source
4
+ //! file is unit-tested by a *colocated* test named after it — `foo.py` →
5
+ //! `foo_test.py` (Python), `foo-bar.ts` → `foo-bar.test.ts` (TypeScript).
6
+ //! [`missing_unit_tests`] walks a tree for a [`Language`] and returns every
7
+ //! source file with no such sibling — an "orphan". Test files are what the
8
+ //! check looks *for*, never subjects.
9
+
10
+ use std::collections::HashSet;
11
+ use std::path::{Path, PathBuf};
12
+
13
+ use anyhow::{Context, Result};
14
+
15
+ /// A language whose unit-test location/naming convention can be checked.
16
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
17
+ pub enum Language {
18
+ /// `foo.py` → colocated `foo_test.py`; `__init__.py` is exempt.
19
+ #[value(name = "python")]
20
+ Python,
21
+ /// `foo-bar.ts` → colocated `foo-bar.test.ts`, across `.ts`/`.tsx`/`.mts`/`.cts`;
22
+ /// declaration files (`.d.ts`/`.d.mts`/`.d.cts`) are ignored.
23
+ #[value(name = "typescript")]
24
+ TypeScript,
25
+ }
26
+
27
+ impl Language {
28
+ /// `true` for a file this language's check tracks (source *or* test).
29
+ fn tracks(self, path: &Path) -> bool {
30
+ match self {
31
+ Language::Python => has_extension(path, &["py"]),
32
+ Language::TypeScript => {
33
+ has_extension(path, &["ts", "tsx", "mts", "cts"]) && !is_declaration(path)
34
+ }
35
+ }
36
+ }
37
+
38
+ /// `true` when `path` is itself a unit test, never a subject.
39
+ fn is_test(self, path: &Path) -> bool {
40
+ match self {
41
+ Language::Python => stem_of(path).ends_with("_test"),
42
+ Language::TypeScript => {
43
+ let name = file_name_of(path);
44
+ name.ends_with(".test.ts")
45
+ || name.ends_with(".test.tsx")
46
+ || name.ends_with(".test.mts")
47
+ || name.ends_with(".test.cts")
48
+ }
49
+ }
50
+ }
51
+
52
+ /// `true` for a file exempt from needing a colocated test.
53
+ fn is_exempt(self, path: &Path) -> bool {
54
+ match self {
55
+ Language::Python => file_name_of(path) == "__init__.py",
56
+ Language::TypeScript => false,
57
+ }
58
+ }
59
+
60
+ /// The colocated test `source` is expected to have.
61
+ fn expected_test_path(self, source: &Path) -> PathBuf {
62
+ match self {
63
+ Language::Python => source.with_file_name(format!("{}_test.py", stem_of(source))),
64
+ Language::TypeScript => {
65
+ source.with_file_name(format!("{}.test.{}", stem_of(source), extension_of(source)))
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ /// Walk `root` recursively and return every source file (for `language`) that
72
+ /// has no colocated unit test, sorted for deterministic output.
73
+ ///
74
+ /// A file that is itself a test is never treated as a subject; every other
75
+ /// source file must have its colocated test sibling. Returns an error if the
76
+ /// tree under `root` cannot be read.
77
+ pub fn missing_unit_tests(root: impl AsRef<Path>, language: Language) -> Result<Vec<PathBuf>> {
78
+ let mut files = Vec::new();
79
+ collect_files(root.as_ref(), language, &mut files)?;
80
+
81
+ // Every tracked path we found, so a subject's expected twin is a lookup
82
+ // rather than a second pass over the filesystem.
83
+ let present: HashSet<&Path> = files.iter().map(PathBuf::as_path).collect();
84
+
85
+ let mut orphans: Vec<PathBuf> = Vec::new();
86
+ for source in &files {
87
+ if language.is_test(source) || language.is_exempt(source) {
88
+ continue;
89
+ }
90
+ if !present.contains(language.expected_test_path(source).as_path()) {
91
+ orphans.push(source.clone());
92
+ }
93
+ }
94
+ orphans.sort();
95
+ Ok(orphans)
96
+ }
97
+
98
+ /// Recursively collect every file `language` tracks under `dir` into `out`.
99
+ fn collect_files(dir: &Path, language: Language, out: &mut Vec<PathBuf>) -> Result<()> {
100
+ let entries =
101
+ std::fs::read_dir(dir).with_context(|| format!("reading directory `{}`", dir.display()))?;
102
+ for entry in entries {
103
+ let path = entry
104
+ .with_context(|| format!("reading an entry under `{}`", dir.display()))?
105
+ .path();
106
+ if path.is_dir() {
107
+ collect_files(&path, language, out)?;
108
+ } else if language.tracks(&path) {
109
+ out.push(path);
110
+ }
111
+ }
112
+ Ok(())
113
+ }
114
+
115
+ /// `true` when the file's extension is one of `extensions`.
116
+ fn has_extension(path: &Path, extensions: &[&str]) -> bool {
117
+ path.extension()
118
+ .and_then(|ext| ext.to_str())
119
+ .is_some_and(|ext| extensions.contains(&ext))
120
+ }
121
+
122
+ /// `true` for a TypeScript declaration file (`*.d.ts` / `*.d.mts` / `*.d.cts`) —
123
+ /// no runtime code, so never a unit-test subject.
124
+ fn is_declaration(path: &Path) -> bool {
125
+ let name = file_name_of(path);
126
+ name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
127
+ }
128
+
129
+ /// The file extension, lossily decoded (empty if there is none).
130
+ fn extension_of(path: &Path) -> String {
131
+ path.extension()
132
+ .map(|ext| ext.to_string_lossy().into_owned())
133
+ .unwrap_or_default()
134
+ }
135
+
136
+ /// The file name, lossily decoded.
137
+ fn file_name_of(path: &Path) -> String {
138
+ path.file_name()
139
+ .map(|name| name.to_string_lossy().into_owned())
140
+ .unwrap_or_default()
141
+ }
142
+
143
+ /// The file stem (the name without its extension), lossily decoded.
144
+ fn stem_of(path: &Path) -> String {
145
+ path.file_stem()
146
+ .map(|stem| stem.to_string_lossy().into_owned())
147
+ .unwrap_or_default()
148
+ }
149
+
150
+ #[cfg(test)]
151
+ mod tests {
152
+ use super::*;
153
+
154
+ #[test]
155
+ fn python_tracks_py_files() {
156
+ assert!(Language::Python.tracks(Path::new("a.py")));
157
+ assert!(Language::Python.tracks(Path::new("pkg/widget.py")));
158
+ assert!(!Language::Python.tracks(Path::new("a.pyi")));
159
+ assert!(!Language::Python.tracks(Path::new("a.txt")));
160
+ assert!(!Language::Python.tracks(Path::new("README")));
161
+ }
162
+
163
+ #[test]
164
+ fn python_recognizes_test_files_by_stem_suffix() {
165
+ assert!(Language::Python.is_test(Path::new("widget_test.py")));
166
+ assert!(Language::Python.is_test(Path::new("pkg/helper_test.py")));
167
+ assert!(!Language::Python.is_test(Path::new("widget.py")));
168
+ }
169
+
170
+ #[test]
171
+ fn python_exempts_the_package_marker() {
172
+ assert!(Language::Python.is_exempt(Path::new("__init__.py")));
173
+ assert!(Language::Python.is_exempt(Path::new("pkg/__init__.py")));
174
+ assert!(!Language::Python.is_exempt(Path::new("conftest.py")));
175
+ assert!(!Language::Python.is_exempt(Path::new("widget.py")));
176
+ }
177
+
178
+ #[test]
179
+ fn python_expected_test_path_is_the_colocated_twin() {
180
+ assert_eq!(
181
+ Language::Python.expected_test_path(Path::new("pkg/widget.py")),
182
+ PathBuf::from("pkg/widget_test.py")
183
+ );
184
+ assert_eq!(
185
+ Language::Python.expected_test_path(Path::new("widget.py")),
186
+ PathBuf::from("widget_test.py")
187
+ );
188
+ }
189
+
190
+ #[test]
191
+ fn typescript_tracks_ts_tsx_mts_cts_but_not_declarations() {
192
+ assert!(Language::TypeScript.tracks(Path::new("widget.ts")));
193
+ assert!(Language::TypeScript.tracks(Path::new("pkg/button.tsx")));
194
+ assert!(Language::TypeScript.tracks(Path::new("service.mts")));
195
+ assert!(Language::TypeScript.tracks(Path::new("legacy.cts")));
196
+ assert!(!Language::TypeScript.tracks(Path::new("types.d.ts")));
197
+ assert!(!Language::TypeScript.tracks(Path::new("ambient.d.mts")));
198
+ assert!(!Language::TypeScript.tracks(Path::new("globals.d.cts")));
199
+ assert!(!Language::TypeScript.tracks(Path::new("widget.py")));
200
+ assert!(!Language::TypeScript.tracks(Path::new("README")));
201
+ }
202
+
203
+ #[test]
204
+ fn typescript_recognizes_test_files_by_suffix() {
205
+ assert!(Language::TypeScript.is_test(Path::new("widget.test.ts")));
206
+ assert!(Language::TypeScript.is_test(Path::new("pkg/button.test.tsx")));
207
+ assert!(Language::TypeScript.is_test(Path::new("service.test.mts")));
208
+ assert!(Language::TypeScript.is_test(Path::new("legacy.test.cts")));
209
+ assert!(!Language::TypeScript.is_test(Path::new("widget.ts")));
210
+ assert!(!Language::TypeScript.is_test(Path::new("button.tsx")));
211
+ assert!(!Language::TypeScript.is_test(Path::new("service.mts")));
212
+ }
213
+
214
+ #[test]
215
+ fn typescript_has_no_exemptions() {
216
+ // Unlike Python's `__init__.py`, nothing in TS is language-mandated;
217
+ // `index.ts` is a deliberate file and therefore a subject.
218
+ assert!(!Language::TypeScript.is_exempt(Path::new("index.ts")));
219
+ assert!(!Language::TypeScript.is_exempt(Path::new("pkg/index.ts")));
220
+ }
221
+
222
+ #[test]
223
+ fn typescript_expected_test_path_keeps_the_extension() {
224
+ assert_eq!(
225
+ Language::TypeScript.expected_test_path(Path::new("pkg/widget.ts")),
226
+ PathBuf::from("pkg/widget.test.ts")
227
+ );
228
+ assert_eq!(
229
+ Language::TypeScript.expected_test_path(Path::new("button.tsx")),
230
+ PathBuf::from("button.test.tsx")
231
+ );
232
+ assert_eq!(
233
+ Language::TypeScript.expected_test_path(Path::new("service.mts")),
234
+ PathBuf::from("service.test.mts")
235
+ );
236
+ assert_eq!(
237
+ Language::TypeScript.expected_test_path(Path::new("legacy.cts")),
238
+ PathBuf::from("legacy.test.cts")
239
+ );
240
+ }
241
+ }
@@ -0,0 +1 @@
1
+ # unit_location clean fixture: nested source colocated with helper_test.py
@@ -0,0 +1 @@
1
+ # unit_location clean fixture: nested unit test for helper.py
@@ -0,0 +1 @@
1
+ # unit_location clean fixture: source colocated with widget_test.py
@@ -0,0 +1 @@
1
+ # unit_location clean fixture: unit test for widget.py
@@ -0,0 +1 @@
1
+ # unit_location exempt fixture: package marker — never a unit-test subject
@@ -0,0 +1 @@
1
+ # unit_location exempt fixture: nested package marker — never a unit-test subject
@@ -0,0 +1 @@
1
+ # unit_location exempt fixture: real source colocated with widget_test.py (OK)
@@ -0,0 +1 @@
1
+ # unit_location exempt fixture: unit test for widget.py
@@ -0,0 +1 @@
1
+ # unit_location red fixture: ORPHAN — no colocated lonely_test.py
@@ -0,0 +1 @@
1
+ # unit_location red fixture: ORPHAN — no colocated orphan_test.py
@@ -0,0 +1 @@
1
+ # unit_location red fixture: nested source colocated with paired_test.py (OK)
@@ -0,0 +1 @@
1
+ # unit_location red fixture: nested unit test for paired.py
@@ -0,0 +1 @@
1
+ # unit_location red fixture: source colocated with widget_test.py (paired, OK)
@@ -0,0 +1 @@
1
+ # unit_location red fixture: unit test for widget.py
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: ESM declaration file (.d.mts) — never a subject, ignored
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: unit test for button.tsx
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: tsx source colocated with button.test.tsx
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: CJS (.cts) source colocated with legacy.test.cts
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: unit test for legacy.cts
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: nested unit test for helper.ts
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: nested source colocated with helper.test.ts
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: ESM (.mts) source colocated with service.test.mts
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: unit test for service.mts
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: declaration file — never a subject, ignored
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: unit test for widget.ts
@@ -0,0 +1 @@
1
+ // unit_location typescript clean fixture: source colocated with widget.test.ts
@@ -0,0 +1 @@
1
+ // unit_location typescript red fixture: CJS (.cts) source with no colocated bridge.test.cts
@@ -0,0 +1 @@
1
+ // unit_location typescript red fixture: ORPHAN — no colocated lonely.test.ts
@@ -0,0 +1 @@
1
+ // unit_location typescript red fixture: nested ORPHAN — no colocated orphan.test.ts
@@ -0,0 +1 @@
1
+ // unit_location typescript red fixture: CJS declaration file (.d.cts) — never a subject, ignored
@@ -0,0 +1 @@
1
+ // unit_location typescript red fixture: declaration file — ignored, not an orphan
@@ -0,0 +1 @@
1
+ // unit_location typescript red fixture: ESM (.mts) source with no colocated solo.test.mts
@@ -0,0 +1 @@
1
+ // unit_location typescript red fixture: unit test for widget.ts
@@ -0,0 +1 @@
1
+ // unit_location typescript red fixture: source colocated with widget.test.ts (OK)
@@ -0,0 +1,125 @@
1
+ //! Integration tests for the unit-test location/naming check
2
+ //! (Python — issue #15; TypeScript — issue #18).
3
+ //!
4
+ //! A source file must have a *colocated* test named after it: `foo.py` →
5
+ //! `foo_test.py`, `foo-bar.ts` → `foo-bar.test.ts`. `missing_unit_tests`
6
+ //! returns the source files missing their twin; the `unit-location`
7
+ //! subcommand turns a non-empty result into a non-zero exit.
8
+ //!
9
+ //! Per the #3 guardrail, each language ships a clean fixture (every source
10
+ //! paired — must pass) and a red fixture (orphans present — must fail).
11
+
12
+ use std::ffi::OsString;
13
+ use std::path::{Path, PathBuf};
14
+
15
+ use testing_conventions::location::{missing_unit_tests, Language};
16
+ use testing_conventions::run;
17
+
18
+ /// Absolute path to a fixture tree under `tests/fixtures/unit_location/`.
19
+ fn fixture(name: &str) -> PathBuf {
20
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
21
+ .join("tests/fixtures/unit_location")
22
+ .join(name)
23
+ }
24
+
25
+ /// Orphans reported under `root` for `language`, as `/`-joined relative paths.
26
+ fn relative_orphans(root: &Path, language: Language) -> Vec<String> {
27
+ missing_unit_tests(root, language)
28
+ .expect("walking a readable tree should succeed")
29
+ .iter()
30
+ .map(|path| {
31
+ path.strip_prefix(root)
32
+ .expect("an orphan must live under the scanned root")
33
+ .to_string_lossy()
34
+ .replace('\\', "/")
35
+ })
36
+ .collect()
37
+ }
38
+
39
+ /// Exit code of `unit-location [--lang <lang>] <fixture>`.
40
+ fn unit_location_exit(fixture_name: &str, lang: Option<&str>) -> i32 {
41
+ let mut argv: Vec<OsString> = vec!["testing-conventions".into(), "unit-location".into()];
42
+ if let Some(lang) = lang {
43
+ argv.push("--lang".into());
44
+ argv.push(lang.into());
45
+ }
46
+ argv.push(fixture(fixture_name).into_os_string());
47
+ run(argv).expect("a readable tree should not error")
48
+ }
49
+
50
+ // ---- Python (#15) --------------------------------------------------------
51
+
52
+ #[test]
53
+ fn python_clean_tree_reports_no_orphans() {
54
+ assert!(
55
+ relative_orphans(&fixture("clean"), Language::Python).is_empty(),
56
+ "every source in the clean tree has a colocated _test.py"
57
+ );
58
+ }
59
+
60
+ #[test]
61
+ fn python_red_tree_reports_every_missing_twin() {
62
+ assert_eq!(
63
+ relative_orphans(&fixture("red"), Language::Python),
64
+ vec!["lonely.py", "pkg/orphan.py"],
65
+ );
66
+ }
67
+
68
+ #[test]
69
+ fn python_package_markers_are_not_orphans() {
70
+ assert!(
71
+ relative_orphans(&fixture("exempt"), Language::Python).is_empty(),
72
+ "__init__.py is a package marker, never a unit-test subject"
73
+ );
74
+ }
75
+
76
+ #[test]
77
+ fn python_missing_root_is_an_error() {
78
+ assert!(
79
+ missing_unit_tests(fixture("does_not_exist"), Language::Python).is_err(),
80
+ "an unreadable root must be an error"
81
+ );
82
+ }
83
+
84
+ #[test]
85
+ fn python_subcommand_exits_zero_on_a_clean_tree() {
86
+ assert_eq!(unit_location_exit("clean", None), 0);
87
+ }
88
+
89
+ #[test]
90
+ fn python_subcommand_exits_nonzero_on_a_red_tree() {
91
+ assert_eq!(unit_location_exit("red", None), 1);
92
+ }
93
+
94
+ // ---- TypeScript (#18) ----------------------------------------------------
95
+
96
+ #[test]
97
+ fn typescript_clean_tree_reports_no_orphans() {
98
+ // The clean tree pairs .ts/.tsx/.mts/.cts sources and holds `*.d.ts` /
99
+ // `*.d.mts` declarations with no twin; declarations must be ignored.
100
+ assert!(
101
+ relative_orphans(&fixture("typescript/clean"), Language::TypeScript).is_empty(),
102
+ "every .ts/.tsx/.mts/.cts source is paired; declaration files are ignored"
103
+ );
104
+ }
105
+
106
+ #[test]
107
+ fn typescript_red_tree_reports_every_missing_twin() {
108
+ assert_eq!(
109
+ relative_orphans(&fixture("typescript/red"), Language::TypeScript),
110
+ vec!["bridge.cts", "lonely.ts", "pkg/orphan.ts", "solo.mts"],
111
+ );
112
+ }
113
+
114
+ #[test]
115
+ fn typescript_subcommand_exits_zero_on_a_clean_tree() {
116
+ assert_eq!(
117
+ unit_location_exit("typescript/clean", Some("typescript")),
118
+ 0
119
+ );
120
+ }
121
+
122
+ #[test]
123
+ fn typescript_subcommand_exits_nonzero_on_a_red_tree() {
124
+ assert_eq!(unit_location_exit("typescript/red", Some("typescript")), 1);
125
+ }
@@ -1,22 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this package are documented here.
4
- Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
-
6
- ## Unreleased
7
-
8
- ### Added
9
-
10
- - `config` module — `load_config()` parses one TOML config file into the new
11
- in-memory `Config` schema (per-language `coverage` thresholds under the
12
- `[python]` / `[typescript]` / `[rust]` tables) and self-validates on load:
13
- unknown keys and malformed TOML are rejected. The parsed config is not
14
- consumed yet. (#12)
15
-
16
- ### Changed
17
-
18
- ### Deprecated
19
-
20
- ### Removed
21
-
22
- ### Fixed