testing-conventions 0.0.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.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: testing-conventions
3
+ Version: 0.0.2
4
+ Summary: Enforce testing conventions in libraries (Python, TypeScript, and Rust).
5
+ Author-email: Kevin Scott <me@thekevinscott.com>
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
9
+ Project-URL: Repository, https://github.com/thekevinscott/testing-conventions
10
+
11
+ # testing-conventions (Python)
12
+
13
+ Non-interactive agent CLI: spec in, code out, no review loop.
14
+
15
+ ```
16
+ pip install testing-conventions
17
+ ```
18
+
@@ -0,0 +1,7 @@
1
+ # testing-conventions (Python)
2
+
3
+ Non-interactive agent CLI: spec in, code out, no review loop.
4
+
5
+ ```
6
+ pip install testing-conventions
7
+ ```
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.7,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "testing-conventions"
7
+ dynamic = ["version"]
8
+ description = "Enforce testing conventions in libraries (Python, TypeScript, and Rust)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Kevin Scott", email = "me@thekevinscott.com" }]
13
+
14
+ [project.urls]
15
+ Repository = "https://github.com/thekevinscott/testing-conventions"
16
+
17
+ [tool.maturin]
18
+ bindings = "bin"
19
+ manifest-path = "rust/Cargo.toml"
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,312 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "anstream"
7
+ version = "1.0.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
10
+ dependencies = [
11
+ "anstyle",
12
+ "anstyle-parse",
13
+ "anstyle-query",
14
+ "anstyle-wincon",
15
+ "colorchoice",
16
+ "is_terminal_polyfill",
17
+ "utf8parse",
18
+ ]
19
+
20
+ [[package]]
21
+ name = "anstyle"
22
+ version = "1.0.14"
23
+ source = "registry+https://github.com/rust-lang/crates.io-index"
24
+ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
25
+
26
+ [[package]]
27
+ name = "anstyle-parse"
28
+ version = "1.0.0"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
31
+ dependencies = [
32
+ "utf8parse",
33
+ ]
34
+
35
+ [[package]]
36
+ name = "anstyle-query"
37
+ version = "1.1.5"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
40
+ dependencies = [
41
+ "windows-sys",
42
+ ]
43
+
44
+ [[package]]
45
+ name = "anstyle-wincon"
46
+ version = "3.0.11"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
49
+ dependencies = [
50
+ "anstyle",
51
+ "once_cell_polyfill",
52
+ "windows-sys",
53
+ ]
54
+
55
+ [[package]]
56
+ name = "anyhow"
57
+ version = "1.0.102"
58
+ source = "registry+https://github.com/rust-lang/crates.io-index"
59
+ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
60
+
61
+ [[package]]
62
+ name = "clap"
63
+ version = "4.6.1"
64
+ source = "registry+https://github.com/rust-lang/crates.io-index"
65
+ checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
66
+ dependencies = [
67
+ "clap_builder",
68
+ "clap_derive",
69
+ ]
70
+
71
+ [[package]]
72
+ name = "clap_builder"
73
+ version = "4.6.0"
74
+ source = "registry+https://github.com/rust-lang/crates.io-index"
75
+ checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
76
+ dependencies = [
77
+ "anstream",
78
+ "anstyle",
79
+ "clap_lex",
80
+ "strsim",
81
+ ]
82
+
83
+ [[package]]
84
+ name = "clap_derive"
85
+ version = "4.6.1"
86
+ source = "registry+https://github.com/rust-lang/crates.io-index"
87
+ checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
88
+ dependencies = [
89
+ "heck",
90
+ "proc-macro2",
91
+ "quote",
92
+ "syn",
93
+ ]
94
+
95
+ [[package]]
96
+ name = "clap_lex"
97
+ version = "1.1.0"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
100
+
101
+ [[package]]
102
+ name = "colorchoice"
103
+ version = "1.0.5"
104
+ source = "registry+https://github.com/rust-lang/crates.io-index"
105
+ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
106
+
107
+ [[package]]
108
+ name = "equivalent"
109
+ version = "1.0.2"
110
+ source = "registry+https://github.com/rust-lang/crates.io-index"
111
+ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
112
+
113
+ [[package]]
114
+ name = "hashbrown"
115
+ version = "0.17.1"
116
+ source = "registry+https://github.com/rust-lang/crates.io-index"
117
+ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
118
+
119
+ [[package]]
120
+ name = "heck"
121
+ version = "0.5.0"
122
+ source = "registry+https://github.com/rust-lang/crates.io-index"
123
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
124
+
125
+ [[package]]
126
+ name = "indexmap"
127
+ version = "2.14.0"
128
+ source = "registry+https://github.com/rust-lang/crates.io-index"
129
+ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
130
+ dependencies = [
131
+ "equivalent",
132
+ "hashbrown",
133
+ ]
134
+
135
+ [[package]]
136
+ name = "is_terminal_polyfill"
137
+ version = "1.70.2"
138
+ source = "registry+https://github.com/rust-lang/crates.io-index"
139
+ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
140
+
141
+ [[package]]
142
+ name = "memchr"
143
+ version = "2.8.1"
144
+ source = "registry+https://github.com/rust-lang/crates.io-index"
145
+ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
146
+
147
+ [[package]]
148
+ name = "once_cell_polyfill"
149
+ version = "1.70.2"
150
+ source = "registry+https://github.com/rust-lang/crates.io-index"
151
+ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
152
+
153
+ [[package]]
154
+ name = "proc-macro2"
155
+ version = "1.0.106"
156
+ source = "registry+https://github.com/rust-lang/crates.io-index"
157
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
158
+ dependencies = [
159
+ "unicode-ident",
160
+ ]
161
+
162
+ [[package]]
163
+ name = "quote"
164
+ version = "1.0.45"
165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
166
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
167
+ dependencies = [
168
+ "proc-macro2",
169
+ ]
170
+
171
+ [[package]]
172
+ name = "serde"
173
+ version = "1.0.228"
174
+ source = "registry+https://github.com/rust-lang/crates.io-index"
175
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
176
+ dependencies = [
177
+ "serde_core",
178
+ "serde_derive",
179
+ ]
180
+
181
+ [[package]]
182
+ name = "serde_core"
183
+ version = "1.0.228"
184
+ source = "registry+https://github.com/rust-lang/crates.io-index"
185
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
186
+ dependencies = [
187
+ "serde_derive",
188
+ ]
189
+
190
+ [[package]]
191
+ name = "serde_derive"
192
+ version = "1.0.228"
193
+ source = "registry+https://github.com/rust-lang/crates.io-index"
194
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
195
+ dependencies = [
196
+ "proc-macro2",
197
+ "quote",
198
+ "syn",
199
+ ]
200
+
201
+ [[package]]
202
+ name = "serde_spanned"
203
+ version = "0.6.9"
204
+ source = "registry+https://github.com/rust-lang/crates.io-index"
205
+ checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
206
+ dependencies = [
207
+ "serde",
208
+ ]
209
+
210
+ [[package]]
211
+ name = "strsim"
212
+ version = "0.11.1"
213
+ source = "registry+https://github.com/rust-lang/crates.io-index"
214
+ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
215
+
216
+ [[package]]
217
+ name = "syn"
218
+ version = "2.0.117"
219
+ source = "registry+https://github.com/rust-lang/crates.io-index"
220
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
221
+ dependencies = [
222
+ "proc-macro2",
223
+ "quote",
224
+ "unicode-ident",
225
+ ]
226
+
227
+ [[package]]
228
+ name = "testing-conventions"
229
+ version = "0.0.2"
230
+ dependencies = [
231
+ "anyhow",
232
+ "clap",
233
+ "serde",
234
+ "toml",
235
+ ]
236
+
237
+ [[package]]
238
+ name = "toml"
239
+ version = "0.8.23"
240
+ source = "registry+https://github.com/rust-lang/crates.io-index"
241
+ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
242
+ dependencies = [
243
+ "serde",
244
+ "serde_spanned",
245
+ "toml_datetime",
246
+ "toml_edit",
247
+ ]
248
+
249
+ [[package]]
250
+ name = "toml_datetime"
251
+ version = "0.6.11"
252
+ source = "registry+https://github.com/rust-lang/crates.io-index"
253
+ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
254
+ dependencies = [
255
+ "serde",
256
+ ]
257
+
258
+ [[package]]
259
+ name = "toml_edit"
260
+ version = "0.22.27"
261
+ source = "registry+https://github.com/rust-lang/crates.io-index"
262
+ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
263
+ dependencies = [
264
+ "indexmap",
265
+ "serde",
266
+ "serde_spanned",
267
+ "toml_datetime",
268
+ "toml_write",
269
+ "winnow",
270
+ ]
271
+
272
+ [[package]]
273
+ name = "toml_write"
274
+ version = "0.1.2"
275
+ source = "registry+https://github.com/rust-lang/crates.io-index"
276
+ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
277
+
278
+ [[package]]
279
+ name = "unicode-ident"
280
+ version = "1.0.24"
281
+ source = "registry+https://github.com/rust-lang/crates.io-index"
282
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
283
+
284
+ [[package]]
285
+ name = "utf8parse"
286
+ version = "0.2.2"
287
+ source = "registry+https://github.com/rust-lang/crates.io-index"
288
+ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
289
+
290
+ [[package]]
291
+ name = "windows-link"
292
+ version = "0.2.1"
293
+ source = "registry+https://github.com/rust-lang/crates.io-index"
294
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
295
+
296
+ [[package]]
297
+ name = "windows-sys"
298
+ version = "0.61.2"
299
+ source = "registry+https://github.com/rust-lang/crates.io-index"
300
+ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
301
+ dependencies = [
302
+ "windows-link",
303
+ ]
304
+
305
+ [[package]]
306
+ name = "winnow"
307
+ version = "0.7.15"
308
+ source = "registry+https://github.com/rust-lang/crates.io-index"
309
+ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
310
+ dependencies = [
311
+ "memchr",
312
+ ]
@@ -0,0 +1,22 @@
1
+ [package]
2
+ name = "testing-conventions"
3
+ version = "0.0.2"
4
+ edition = "2021"
5
+ license = "MIT"
6
+ authors = ["Kevin Scott <me@thekevinscott.com>"]
7
+ repository = "https://github.com/thekevinscott/testing-conventions"
8
+ description = "Enforce testing conventions in libraries (Python, TypeScript, and Rust)."
9
+
10
+ [lib]
11
+ name = "testing_conventions"
12
+ path = "src/lib.rs"
13
+
14
+ [[bin]]
15
+ name = "testing-conventions"
16
+ path = "src/main.rs"
17
+
18
+ [dependencies]
19
+ clap = { version = "4.5", features = ["derive"] }
20
+ anyhow = "1.0"
21
+ serde = { version = "1.0", features = ["derive"] }
22
+ toml = "0.8"
@@ -0,0 +1,43 @@
1
+ # Migrations
2
+
3
+ Upgrade notes for breaking changes. New entries go under `## Unreleased`.
4
+ On release, the section is renamed to `## v<OLD> → v<NEW>`.
5
+
6
+ Each entry has five sections, in order:
7
+
8
+ 1. **Summary** — one paragraph: what changed and why.
9
+ 2. **Required changes** — before/after for public API. "None" if purely additive.
10
+ 3. **Deprecations removed** — anything previously warned about that's now gone.
11
+ 4. **Behavior changes without code changes** — same API, different runtime behavior.
12
+ 5. **Verification** — commands that confirm the upgrade worked, with expected output.
13
+
14
+ ## Unreleased
15
+
16
+ ### Summary
17
+
18
+ Adds the `config` module: a `Config` schema holding the per-language `coverage`
19
+ thresholds (`[python]` / `[typescript]` / `[rust]`), plus `load_config()`, which
20
+ reads one TOML file into it and validates the config itself (the self-guard) —
21
+ unknown keys and malformed TOML are rejected rather than silently accepted.
22
+ Purely additive; nothing consumes the parsed config yet.
23
+
24
+ ### Required changes
25
+
26
+ None. New, additive API: `testing_conventions::config::{Config, load_config}`.
27
+
28
+ ### Deprecations removed
29
+
30
+ None.
31
+
32
+ ### Behavior changes without code changes
33
+
34
+ None.
35
+
36
+ ### Verification
37
+
38
+ ```
39
+ cd packages/rust && cargo test --test config_loader
40
+ ```
41
+
42
+ Expected: the loader's integration tests pass — the canonical config loads into
43
+ memory, and unknown-key, malformed, and missing-file configs are rejected.
@@ -0,0 +1,89 @@
1
+ //! The testing-conventions config schema and loader.
2
+ //!
3
+ //! One config file is read into the in-memory [`Config`] below. The loader
4
+ //! parses *and* validates the config itself (the "self-guard" from issue #12):
5
+ //! a malformed or unknown-key config is an error, never a silently-accepted
6
+ //! default.
7
+ //!
8
+ //! Nothing consumes [`Config`] yet — this module only turns the file on disk
9
+ //! into the in-memory structure.
10
+
11
+ use std::path::Path;
12
+
13
+ use anyhow::{Context, Result};
14
+ use serde::Deserialize;
15
+
16
+ /// A fully-parsed testing-conventions config file.
17
+ ///
18
+ /// Holds the per-language coverage thresholds — the `[python]` / `[typescript]`
19
+ /// / `[rust]` tables from the README's "Configuration" section. Each table is
20
+ /// optional so a repo can configure only the languages it ships. Test locations
21
+ /// follow convention, not config, so there are no location keys here.
22
+ #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
23
+ #[serde(deny_unknown_fields)]
24
+ pub struct Config {
25
+ pub python: Option<PythonConfig>,
26
+ pub typescript: Option<TypeScriptConfig>,
27
+ pub rust: Option<RustConfig>,
28
+ }
29
+
30
+ /// The `[python]` table.
31
+ #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
32
+ #[serde(deny_unknown_fields)]
33
+ pub struct PythonConfig {
34
+ pub coverage: PythonCoverage,
35
+ }
36
+
37
+ /// The `[typescript]` table.
38
+ #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
39
+ #[serde(deny_unknown_fields)]
40
+ pub struct TypeScriptConfig {
41
+ pub coverage: TypeScriptCoverage,
42
+ }
43
+
44
+ /// The `[rust]` table.
45
+ #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
46
+ #[serde(deny_unknown_fields)]
47
+ pub struct RustConfig {
48
+ pub coverage: RustCoverage,
49
+ }
50
+
51
+ /// `[python].coverage`.
52
+ #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
53
+ #[serde(deny_unknown_fields)]
54
+ pub struct PythonCoverage {
55
+ pub branch: bool,
56
+ pub fail_under: u8,
57
+ }
58
+
59
+ /// `[typescript].coverage`.
60
+ #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
61
+ #[serde(deny_unknown_fields)]
62
+ pub struct TypeScriptCoverage {
63
+ pub lines: u8,
64
+ pub branches: u8,
65
+ pub functions: u8,
66
+ pub statements: u8,
67
+ }
68
+
69
+ /// `[rust].coverage`. Branch coverage is still experimental, so only
70
+ /// regions/lines are configurable.
71
+ #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
72
+ #[serde(deny_unknown_fields)]
73
+ pub struct RustCoverage {
74
+ pub regions: u8,
75
+ pub lines: u8,
76
+ }
77
+
78
+ /// Read one config file at `path` into a [`Config`], validating it on the way.
79
+ ///
80
+ /// The validation is the config's self-guard: `serde`'s `deny_unknown_fields`
81
+ /// rejects keys that aren't part of the schema, missing required keys and
82
+ /// wrong-typed values are type errors, and malformed TOML fails to parse. Any
83
+ /// of these surfaces as an `Err` rather than a silently-accepted default.
84
+ pub fn load_config(path: impl AsRef<Path>) -> Result<Config> {
85
+ let path = path.as_ref();
86
+ let contents = std::fs::read_to_string(path)
87
+ .with_context(|| format!("reading config file `{}`", path.display()))?;
88
+ toml::from_str(&contents).with_context(|| format!("parsing config file `{}`", path.display()))
89
+ }
@@ -0,0 +1,71 @@
1
+ pub mod config;
2
+
3
+ use clap::{Parser, Subcommand};
4
+
5
+ #[derive(Parser, Debug)]
6
+ #[command(
7
+ name = "testing-conventions",
8
+ version,
9
+ about = "Enforce testing conventions in libraries (Python, TypeScript, and Rust).",
10
+ long_about = None,
11
+ )]
12
+ pub struct Cli {
13
+ #[command(subcommand)]
14
+ command: Option<Command>,
15
+ }
16
+
17
+ #[derive(Subcommand, Debug)]
18
+ enum Command {
19
+ /// Check the repository against its testing-conventions config.
20
+ Check,
21
+ }
22
+
23
+ pub fn run<I, T>(args: I) -> anyhow::Result<i32>
24
+ where
25
+ I: IntoIterator<Item = T>,
26
+ T: Into<std::ffi::OsString> + Clone,
27
+ {
28
+ let cli = Cli::try_parse_from(args)?;
29
+ match cli.command {
30
+ // Rules aren't implemented yet; the scaffold just proves the wiring.
31
+ Some(Command::Check) | None => Ok(0),
32
+ }
33
+ }
34
+
35
+ #[cfg(test)]
36
+ mod tests {
37
+ use super::*;
38
+
39
+ #[test]
40
+ fn no_args_returns_ok_zero() {
41
+ assert_eq!(run(["testing-conventions"]).unwrap(), 0);
42
+ }
43
+
44
+ #[test]
45
+ fn check_returns_ok_zero() {
46
+ assert_eq!(run(["testing-conventions", "check"]).unwrap(), 0);
47
+ }
48
+
49
+ #[test]
50
+ fn unknown_flag_errors() {
51
+ assert!(run(["testing-conventions", "--bogus"]).is_err());
52
+ }
53
+
54
+ #[test]
55
+ fn help_flag_returns_clap_display_help() {
56
+ let err = run(["testing-conventions", "--help"]).expect_err("--help should bubble");
57
+ let clap_err = err
58
+ .downcast_ref::<clap::Error>()
59
+ .expect("error should be a clap::Error");
60
+ assert_eq!(clap_err.kind(), clap::error::ErrorKind::DisplayHelp);
61
+ }
62
+
63
+ #[test]
64
+ fn version_flag_returns_clap_display_version() {
65
+ let err = run(["testing-conventions", "--version"]).expect_err("--version should bubble");
66
+ let clap_err = err
67
+ .downcast_ref::<clap::Error>()
68
+ .expect("error should be a clap::Error");
69
+ assert_eq!(clap_err.kind(), clap::error::ErrorKind::DisplayVersion);
70
+ }
71
+ }
@@ -0,0 +1,14 @@
1
+ use std::process::ExitCode;
2
+
3
+ fn main() -> ExitCode {
4
+ match testing_conventions::run(std::env::args_os()) {
5
+ Ok(code) => ExitCode::from(code as u8),
6
+ Err(err) => {
7
+ if let Some(clap_err) = err.downcast_ref::<clap::Error>() {
8
+ clap_err.exit();
9
+ }
10
+ eprintln!("error: {err}");
11
+ ExitCode::from(1)
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,82 @@
1
+ //! Integration tests for the config schema + loader (issue #12).
2
+ //!
3
+ //! These pin the contract from the README's "Configuration" section: one config
4
+ //! file is read into the in-memory `Config`, and the self-guard rejects a config
5
+ //! that fails its own validation (unknown keys, malformed TOML) rather than
6
+ //! silently accepting it.
7
+ //!
8
+ //! Per the #3 guardrail, the loader ships a clean fixture (`valid.toml`, must
9
+ //! load) and red fixtures (`unknown_key.toml` / `malformed.toml`, must fail).
10
+
11
+ use std::path::PathBuf;
12
+
13
+ use testing_conventions::config::{
14
+ load_config, Config, PythonConfig, PythonCoverage, RustConfig, RustCoverage, TypeScriptConfig,
15
+ TypeScriptCoverage,
16
+ };
17
+
18
+ /// Absolute path to a file under `tests/fixtures/`.
19
+ fn fixture(name: &str) -> PathBuf {
20
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
21
+ .join("tests/fixtures")
22
+ .join(name)
23
+ }
24
+
25
+ /// The in-memory shape we expect `valid.toml` to parse into.
26
+ fn expected_valid() -> Config {
27
+ Config {
28
+ python: Some(PythonConfig {
29
+ coverage: PythonCoverage {
30
+ branch: true,
31
+ fail_under: 100,
32
+ },
33
+ }),
34
+ typescript: Some(TypeScriptConfig {
35
+ coverage: TypeScriptCoverage {
36
+ lines: 100,
37
+ branches: 100,
38
+ functions: 100,
39
+ statements: 100,
40
+ },
41
+ }),
42
+ rust: Some(RustConfig {
43
+ coverage: RustCoverage {
44
+ regions: 100,
45
+ lines: 100,
46
+ },
47
+ }),
48
+ }
49
+ }
50
+
51
+ #[test]
52
+ fn loads_the_canonical_config_into_memory() {
53
+ let config = load_config(fixture("valid.toml")).expect("the canonical config should load");
54
+ assert_eq!(config, expected_valid());
55
+ }
56
+
57
+ #[test]
58
+ fn rejects_unknown_keys_self_guard() {
59
+ let result = load_config(fixture("unknown_key.toml"));
60
+ assert!(
61
+ result.is_err(),
62
+ "an unknown config key must be rejected (self-guard), got: {result:?}"
63
+ );
64
+ }
65
+
66
+ #[test]
67
+ fn rejects_malformed_toml() {
68
+ let result = load_config(fixture("malformed.toml"));
69
+ assert!(
70
+ result.is_err(),
71
+ "malformed TOML must be rejected, got: {result:?}"
72
+ );
73
+ }
74
+
75
+ #[test]
76
+ fn errors_on_a_missing_file() {
77
+ let result = load_config(fixture("does_not_exist.toml"));
78
+ assert!(
79
+ result.is_err(),
80
+ "a missing config file must be an error, got: {result:?}"
81
+ );
82
+ }
@@ -0,0 +1,4 @@
1
+ # Red fixture: not valid TOML (unterminated table header, dangling value).
2
+ # The loader must surface the parse error, not swallow it.
3
+ [python
4
+ unit.glob =
@@ -0,0 +1,6 @@
1
+ # Red fixture: a valid coverage config plus one key the schema doesn't know
2
+ # (`unknown`). The loader's self-guard (serde `deny_unknown_fields`) must reject
3
+ # it — keeping the rest valid means this fixture fails *only* on the bad key.
4
+ [python]
5
+ coverage = { branch = true, fail_under = 100 }
6
+ unknown = true
@@ -0,0 +1,9 @@
1
+ # Clean fixture: per-language coverage thresholds. Must load (issue #12).
2
+ [python]
3
+ coverage = { branch = true, fail_under = 100 }
4
+
5
+ [typescript]
6
+ coverage = { lines = 100, branches = 100, functions = 100, statements = 100 }
7
+
8
+ [rust]
9
+ coverage = { regions = 100, lines = 100 }