Skip to main content

dev_tools/
lib.rs

1//! # dev-tools
2//!
3//! Modular verification toolkit for AI-assisted Rust development.
4//! Umbrella crate over the `dev-*` suite.
5//!
6//! `dev-tools` is the convenient one-import entry point. Pick the
7//! features you need and pull them in with one line.
8//!
9//! ## Default features
10//!
11//! By default, you get:
12//!
13//! - [`mod@report`]: structured machine-readable verdicts (always enabled).
14//! - [`mod@fixtures`]: deterministic test environments.
15//! - [`mod@bench`]: performance measurement and regression detection.
16//!
17//! ## Opt-in features
18//!
19//! Enable with `features = ["..."]`:
20//!
21//! - `async`: async-specific validation (deadlocks, hung futures, leaks).
22//! - `stress`: high-load stress testing (concurrency, volume).
23//! - `chaos`: failure injection and recovery testing.
24//! - `full`: all of the above.
25//!
26//! ## Quick example
27//!
28//! ```toml
29//! [dependencies]
30//! dev-tools = "0.9.2"
31//! ```
32//!
33//! ```rust
34//! use dev_tools::report::{Report, Verdict};
35//!
36//! let mut r = Report::new("my-crate", "0.1.0");
37//! // ... use r ...
38//! ```
39//!
40//! ## See also
41//!
42//! - [`dev-report`](https://crates.io/crates/dev-report) - schema only
43//! - [`dev-fixtures`](https://crates.io/crates/dev-fixtures) - test environments
44//! - [`dev-bench`](https://crates.io/crates/dev-bench) - performance
45//! - [`dev-async`](https://crates.io/crates/dev-async) - async validation
46//! - [`dev-stress`](https://crates.io/crates/dev-stress) - load testing
47//! - [`dev-chaos`](https://crates.io/crates/dev-chaos) - failure injection
48
49#![cfg_attr(docsrs, feature(doc_cfg))]
50#![warn(missing_docs)]
51#![warn(rust_2018_idioms)]
52
53/// Re-export of [`dev_report`]. Always available.
54pub use dev_report as report;
55
56/// Re-export of [`dev_fixtures`]. Available with the `fixtures` feature.
57#[cfg(feature = "fixtures")]
58#[cfg_attr(docsrs, doc(cfg(feature = "fixtures")))]
59pub use dev_fixtures as fixtures;
60
61/// Re-export of [`dev_bench`]. Available with the `bench` feature.
62#[cfg(feature = "bench")]
63#[cfg_attr(docsrs, doc(cfg(feature = "bench")))]
64pub use dev_bench as bench;
65
66/// Re-export of [`dev_async`]. Available with the `async` feature.
67#[cfg(feature = "async")]
68#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
69pub use dev_async as r#async;
70
71/// Re-export of [`dev_stress`]. Available with the `stress` feature.
72#[cfg(feature = "stress")]
73#[cfg_attr(docsrs, doc(cfg(feature = "stress")))]
74pub use dev_stress as stress;
75
76/// Re-export of [`dev_chaos`]. Available with the `chaos` feature.
77#[cfg(feature = "chaos")]
78#[cfg_attr(docsrs, doc(cfg(feature = "chaos")))]
79pub use dev_chaos as chaos;
80
81/// Convenience re-exports for the most common items across the suite.
82///
83/// `use dev_tools::prelude::*;` to pull in the schema types
84/// ([`Report`], [`CheckResult`], [`Verdict`], [`Severity`], [`Evidence`],
85/// the [`Producer`] trait) plus `MultiReport` and `Diff`. Optional
86/// per-feature items (`fixtures::TempProject`, `bench::Benchmark`,
87/// etc.) are NOT in the prelude — pull them in directly via the
88/// re-exported sub-crate modules.
89///
90/// # Example
91///
92/// ```
93/// use dev_tools::prelude::*;
94///
95/// let mut r = Report::new("my-crate", "0.1.0");
96/// r.push(CheckResult::pass("compile"));
97/// r.finish();
98/// assert!(r.passed());
99/// ```
100///
101/// [`Report`]: dev_report::Report
102/// [`CheckResult`]: dev_report::CheckResult
103/// [`Verdict`]: dev_report::Verdict
104/// [`Severity`]: dev_report::Severity
105/// [`Evidence`]: dev_report::Evidence
106/// [`Producer`]: dev_report::Producer
107pub mod prelude {
108    pub use dev_report::{
109        CheckResult, Diff, DiffOptions, DurationRegression, Evidence, EvidenceData, EvidenceKind,
110        FileRef, MultiReport, Producer, Report, Severity, SeverityChange, Verdict,
111    };
112
113    /// Async-flavored prelude. Available with the `async` feature.
114    ///
115    /// Pulls in the standard prelude plus `dev_async`'s
116    /// `AsyncCheck`, `AsyncProducer`, and `BlockingAsyncProducer`
117    /// types so callers driving async producers don't have to
118    /// import them individually.
119    ///
120    /// # Example
121    ///
122    /// ```ignore
123    /// use dev_tools::prelude::async_prelude::*;
124    ///
125    /// // run_with_timeout, BlockingAsyncProducer, etc. all in scope
126    /// ```
127    #[cfg(feature = "async")]
128    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
129    pub mod async_prelude {
130        pub use super::*;
131        pub use dev_async::{
132            join_all_with_timeout, run_with_timeout, AsyncCheck, AsyncProducer,
133            BlockingAsyncProducer,
134        };
135    }
136}
137
138/// Combine multiple `dev_report::Producer` results into a single
139/// `MultiReport` keyed by `subject`/`version`.
140///
141/// Pure composition: no new types, no new logic. Each producer is
142/// invoked once via `Producer::produce()` and pushed into the
143/// returned [`dev_report::MultiReport`].
144///
145/// # Example
146///
147/// ```
148/// use dev_tools::full_run;
149/// use dev_tools::report::{CheckResult, Producer, Report, Verdict};
150///
151/// struct A;
152/// impl Producer for A {
153///     fn produce(&self) -> Report {
154///         let mut r = Report::new("crate", "0.1.0").with_producer("a");
155///         r.push(CheckResult::pass("ok"));
156///         r.finish();
157///         r
158///     }
159/// }
160/// struct B;
161/// impl Producer for B {
162///     fn produce(&self) -> Report {
163///         let mut r = Report::new("crate", "0.1.0").with_producer("b");
164///         r.push(CheckResult::pass("ok"));
165///         r.finish();
166///         r
167///     }
168/// }
169///
170/// let multi = full_run!("crate", "0.1.0"; A, B);
171/// assert_eq!(multi.reports.len(), 2);
172/// assert_eq!(multi.overall_verdict(), Verdict::Pass);
173/// ```
174#[macro_export]
175macro_rules! full_run {
176    ($subject:expr, $version:expr; $($producer:expr),* $(,)?) => {{
177        let mut multi = $crate::report::MultiReport::new($subject, $version);
178        $(
179            multi.push(<_ as $crate::report::Producer>::produce(&$producer));
180        )*
181        multi.finish();
182        multi
183    }};
184}
185
186/// Combine multiple `Future<Output = Report>` values into a single
187/// `MultiReport` keyed by `subject`/`version`.
188///
189/// Async equivalent of [`full_run!`] for callers already inside an
190/// async context. Each future is awaited in sequence (use a
191/// futures-runtime helper if you need concurrency); the resulting
192/// reports are pushed into the returned [`dev_report::MultiReport`].
193///
194/// Available with the `async` feature.
195///
196/// # Example
197///
198/// ```ignore
199/// use dev_tools::async_full_run;
200/// use dev_tools::report::{CheckResult, Report, Verdict};
201///
202/// async fn produce_a() -> Report {
203///     let mut r = Report::new("crate", "0.1.0").with_producer("a");
204///     r.push(CheckResult::pass("ok"));
205///     r.finish();
206///     r
207/// }
208///
209/// async fn produce_b() -> Report {
210///     let mut r = Report::new("crate", "0.1.0").with_producer("b");
211///     r.push(CheckResult::pass("ok"));
212///     r.finish();
213///     r
214/// }
215///
216/// # async fn ex() {
217/// let multi = async_full_run!("crate", "0.1.0"; produce_a(), produce_b()).await;
218/// assert_eq!(multi.reports.len(), 2);
219/// assert_eq!(multi.overall_verdict(), Verdict::Pass);
220/// # }
221/// ```
222#[cfg(feature = "async")]
223#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
224#[macro_export]
225macro_rules! async_full_run {
226    ($subject:expr, $version:expr; $($fut:expr),* $(,)?) => {{
227        async {
228            let mut multi = $crate::report::MultiReport::new($subject, $version);
229            $(
230                multi.push($fut.await);
231            )*
232            multi.finish();
233            multi
234        }
235    }};
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn report_module_is_always_available() {
244        let r = report::Report::new("self", "0.1.0");
245        assert_eq!(r.subject, "self");
246    }
247
248    #[test]
249    fn prelude_pulls_core_types() {
250        // The prelude should make these immediately accessible
251        // without further imports.
252        use crate::prelude::*;
253
254        let mut r = Report::new("c", "0.1.0");
255        r.push(CheckResult::pass("ok"));
256        r.finish();
257        assert_eq!(r.overall_verdict(), Verdict::Pass);
258        assert!(r.passed());
259
260        let _ev = Evidence::numeric_int("count", 42);
261        let _opts = DiffOptions::default();
262        let _multi = MultiReport::new("c", "0.1.0");
263
264        // 0.9.2: also includes DurationRegression and SeverityChange.
265        let _dr: Option<DurationRegression> = None;
266        let _sc: Option<SeverityChange> = None;
267
268        // Sanity-check that Severity and Producer/Diff/etc. are in scope.
269        let _sev = Severity::Error;
270        fn _takes_producer(_p: &dyn Producer) {}
271        fn _takes_diff(_d: &Diff) {}
272    }
273
274    #[cfg(feature = "fixtures")]
275    #[test]
276    fn fixtures_module_is_available_with_feature() {
277        let _ = fixtures::TempProject::new();
278    }
279
280    #[cfg(feature = "bench")]
281    #[test]
282    fn bench_module_is_available_with_feature() {
283        let _ = bench::Benchmark::new("x");
284    }
285
286    #[test]
287    fn full_run_combines_zero_producers() {
288        let multi = full_run!("crate", "0.1.0";);
289        assert_eq!(multi.reports.len(), 0);
290        assert_eq!(multi.overall_verdict(), report::Verdict::Skip);
291    }
292
293    #[test]
294    fn full_run_combines_two_producers() {
295        struct OkProducer(&'static str);
296        impl report::Producer for OkProducer {
297            fn produce(&self) -> report::Report {
298                let mut r = report::Report::new("c", "0.1.0").with_producer(self.0);
299                r.push(report::CheckResult::pass("x"));
300                r.finish();
301                r
302            }
303        }
304        let multi = full_run!("c", "0.1.0"; OkProducer("a"), OkProducer("b"));
305        assert_eq!(multi.reports.len(), 2);
306        assert_eq!(multi.overall_verdict(), report::Verdict::Pass);
307    }
308
309    #[test]
310    fn full_run_propagates_failures() {
311        struct OkProducer;
312        impl report::Producer for OkProducer {
313            fn produce(&self) -> report::Report {
314                let mut r = report::Report::new("c", "0.1.0").with_producer("ok");
315                r.push(report::CheckResult::pass("x"));
316                r.finish();
317                r
318            }
319        }
320        struct FailProducer;
321        impl report::Producer for FailProducer {
322            fn produce(&self) -> report::Report {
323                let mut r = report::Report::new("c", "0.1.0").with_producer("fail");
324                r.push(report::CheckResult::fail("y", report::Severity::Error));
325                r.finish();
326                r
327            }
328        }
329        let multi = full_run!("c", "0.1.0"; OkProducer, FailProducer);
330        assert_eq!(multi.overall_verdict(), report::Verdict::Fail);
331    }
332
333    #[cfg(all(feature = "fixtures", feature = "bench"))]
334    #[test]
335    fn full_run_with_real_producers() {
336        // fixtures: a self-test of TempProject lifecycle.
337        let fixture_producer =
338            fixtures::FixtureProducer::new("temp_project_lifecycle", "0.1.0", || {
339                let _p = fixtures::TempProject::new()
340                    .with_file("README.md", "hi")
341                    .build()?;
342                Ok(())
343            });
344        // bench: a tiny benchmark with no baseline.
345        let bench_producer = bench::BenchProducer::new(
346            || {
347                let mut b = bench::Benchmark::new("hot");
348                for _ in 0..5 {
349                    b.iter(|| std::hint::black_box(1 + 1));
350                }
351                b.finish()
352            },
353            "0.1.0",
354            None,
355            bench::Threshold::regression_pct(20.0),
356        );
357        let multi = full_run!("crate", "0.1.0"; fixture_producer, bench_producer);
358        assert_eq!(multi.reports.len(), 2);
359    }
360
361    #[cfg(feature = "async")]
362    #[test]
363    fn async_full_run_compiles() {
364        // Compile-time check that async_full_run! expands cleanly.
365        // We don't drive the future here (no runtime in dev-deps), but
366        // compilation alone is meaningful: it catches macro-hygiene bugs.
367        async fn produce_a() -> report::Report {
368            let mut r = report::Report::new("c", "0.1.0").with_producer("a");
369            r.push(report::CheckResult::pass("x"));
370            r.finish();
371            r
372        }
373        let _fut = async_full_run!("c", "0.1.0"; produce_a(), produce_a());
374        // Drop the future without polling; compiles cleanly.
375    }
376}