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
56pub mod brand;
57pub mod html;
58pub mod producers;
59
60/// Extension trait providing [`to_html`](Self::to_html) on
61/// [`dev_report::MultiReport`].
62///
63/// `MultiReport` lives in `dev-report` (the schema crate, which is kept
64/// dependency-free), so the HTML meta-report renderer lives here in
65/// `dev-tools` and exposes itself through this extension trait.
66///
67/// Pull it in via `use dev_tools::MultiReportHtmlExt;` or via the
68/// prelude.
69///
70/// # Example
71///
72/// ```
73/// use dev_report::{CheckResult, MultiReport, Report};
74/// use dev_tools::MultiReportHtmlExt;
75///
76/// let mut r = Report::new("crate", "0.1.0").with_producer("dev-bench");
77/// r.push(CheckResult::pass("ok"));
78/// let mut multi = MultiReport::new("crate", "0.1.0");
79/// multi.push(r);
80///
81/// let html = multi.to_html();
82/// assert!(html.starts_with("<!DOCTYPE html>"));
83/// ```
84pub trait MultiReportHtmlExt {
85    /// Render this `MultiReport` as a self-contained HTML document.
86    ///
87    /// See [`html`] for output guarantees (no external assets,
88    /// deterministic output, CSS custom properties driven by the
89    /// [`brand`] module).
90    fn to_html(&self) -> String;
91}
92
93impl MultiReportHtmlExt for dev_report::MultiReport {
94    fn to_html(&self) -> String {
95        html::multi_report_to_html(self)
96    }
97}
98
99/// Re-export of [`dev_fixtures`]. Available with the `fixtures` feature.
100#[cfg(feature = "fixtures")]
101#[cfg_attr(docsrs, doc(cfg(feature = "fixtures")))]
102pub use dev_fixtures as fixtures;
103
104/// Re-export of [`dev_bench`]. Available with the `bench` feature.
105#[cfg(feature = "bench")]
106#[cfg_attr(docsrs, doc(cfg(feature = "bench")))]
107pub use dev_bench as bench;
108
109/// Re-export of [`dev_async`]. Available with the `async` feature.
110#[cfg(feature = "async")]
111#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
112pub use dev_async as r#async;
113
114/// Re-export of [`dev_stress`]. Available with the `stress` feature.
115#[cfg(feature = "stress")]
116#[cfg_attr(docsrs, doc(cfg(feature = "stress")))]
117pub use dev_stress as stress;
118
119/// Re-export of [`dev_chaos`]. Available with the `chaos` feature.
120#[cfg(feature = "chaos")]
121#[cfg_attr(docsrs, doc(cfg(feature = "chaos")))]
122pub use dev_chaos as chaos;
123
124/// Convenience re-exports for the most common items across the suite.
125///
126/// `use dev_tools::prelude::*;` to pull in the schema types
127/// ([`Report`], [`CheckResult`], [`Verdict`], [`Severity`], [`Evidence`],
128/// the [`Producer`] trait) plus `MultiReport` and `Diff`. Optional
129/// per-feature items (`fixtures::TempProject`, `bench::Benchmark`,
130/// etc.) are NOT in the prelude — pull them in directly via the
131/// re-exported sub-crate modules.
132///
133/// # Example
134///
135/// ```
136/// use dev_tools::prelude::*;
137///
138/// let mut r = Report::new("my-crate", "0.1.0");
139/// r.push(CheckResult::pass("compile"));
140/// r.finish();
141/// assert!(r.passed());
142/// ```
143///
144/// [`Report`]: dev_report::Report
145/// [`CheckResult`]: dev_report::CheckResult
146/// [`Verdict`]: dev_report::Verdict
147/// [`Severity`]: dev_report::Severity
148/// [`Evidence`]: dev_report::Evidence
149/// [`Producer`]: dev_report::Producer
150pub mod prelude {
151    pub use dev_report::{
152        CheckResult, Diff, DiffOptions, DurationRegression, Evidence, EvidenceData, EvidenceKind,
153        FileRef, MultiReport, Producer, Report, Severity, SeverityChange, Verdict,
154    };
155
156    pub use crate::MultiReportHtmlExt;
157
158    /// Async-flavored prelude. Available with the `async` feature.
159    ///
160    /// Pulls in the standard prelude plus `dev_async`'s
161    /// `AsyncCheck`, `AsyncProducer`, and `BlockingAsyncProducer`
162    /// types so callers driving async producers don't have to
163    /// import them individually.
164    ///
165    /// # Example
166    ///
167    /// ```ignore
168    /// use dev_tools::prelude::async_prelude::*;
169    ///
170    /// // run_with_timeout, BlockingAsyncProducer, etc. all in scope
171    /// ```
172    #[cfg(feature = "async")]
173    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
174    pub mod async_prelude {
175        pub use super::*;
176        pub use dev_async::{
177            join_all_with_timeout, run_with_timeout, AsyncCheck, AsyncProducer,
178            BlockingAsyncProducer,
179        };
180    }
181}
182
183/// Combine multiple `dev_report::Producer` results into a single
184/// `MultiReport` keyed by `subject`/`version`.
185///
186/// Pure composition: no new types, no new logic. Each producer is
187/// invoked once via `Producer::produce()` and pushed into the
188/// returned [`dev_report::MultiReport`].
189///
190/// # Example
191///
192/// ```
193/// use dev_tools::full_run;
194/// use dev_tools::report::{CheckResult, Producer, Report, Verdict};
195///
196/// struct A;
197/// impl Producer for A {
198///     fn produce(&self) -> Report {
199///         let mut r = Report::new("crate", "0.1.0").with_producer("a");
200///         r.push(CheckResult::pass("ok"));
201///         r.finish();
202///         r
203///     }
204/// }
205/// struct B;
206/// impl Producer for B {
207///     fn produce(&self) -> Report {
208///         let mut r = Report::new("crate", "0.1.0").with_producer("b");
209///         r.push(CheckResult::pass("ok"));
210///         r.finish();
211///         r
212///     }
213/// }
214///
215/// let multi = full_run!("crate", "0.1.0"; A, B);
216/// assert_eq!(multi.reports.len(), 2);
217/// assert_eq!(multi.overall_verdict(), Verdict::Pass);
218/// ```
219#[macro_export]
220macro_rules! full_run {
221    ($subject:expr, $version:expr; $($producer:expr),* $(,)?) => {{
222        let mut multi = $crate::report::MultiReport::new($subject, $version);
223        $(
224            multi.push(<_ as $crate::report::Producer>::produce(&$producer));
225        )*
226        multi.finish();
227        multi
228    }};
229}
230
231/// Combine multiple `Future<Output = Report>` values into a single
232/// `MultiReport` keyed by `subject`/`version`.
233///
234/// Async equivalent of [`full_run!`] for callers already inside an
235/// async context. Each future is awaited in sequence (use a
236/// futures-runtime helper if you need concurrency); the resulting
237/// reports are pushed into the returned [`dev_report::MultiReport`].
238///
239/// Available with the `async` feature.
240///
241/// # Example
242///
243/// ```ignore
244/// use dev_tools::async_full_run;
245/// use dev_tools::report::{CheckResult, Report, Verdict};
246///
247/// async fn produce_a() -> Report {
248///     let mut r = Report::new("crate", "0.1.0").with_producer("a");
249///     r.push(CheckResult::pass("ok"));
250///     r.finish();
251///     r
252/// }
253///
254/// async fn produce_b() -> Report {
255///     let mut r = Report::new("crate", "0.1.0").with_producer("b");
256///     r.push(CheckResult::pass("ok"));
257///     r.finish();
258///     r
259/// }
260///
261/// # async fn ex() {
262/// let multi = async_full_run!("crate", "0.1.0"; produce_a(), produce_b()).await;
263/// assert_eq!(multi.reports.len(), 2);
264/// assert_eq!(multi.overall_verdict(), Verdict::Pass);
265/// # }
266/// ```
267#[cfg(feature = "async")]
268#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
269#[macro_export]
270macro_rules! async_full_run {
271    ($subject:expr, $version:expr; $($fut:expr),* $(,)?) => {{
272        async {
273            let mut multi = $crate::report::MultiReport::new($subject, $version);
274            $(
275                multi.push($fut.await);
276            )*
277            multi.finish();
278            multi
279        }
280    }};
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn report_module_is_always_available() {
289        let r = report::Report::new("self", "0.1.0");
290        assert_eq!(r.subject, "self");
291    }
292
293    #[test]
294    fn prelude_pulls_core_types() {
295        // The prelude should make these immediately accessible
296        // without further imports.
297        use crate::prelude::*;
298
299        let mut r = Report::new("c", "0.1.0");
300        r.push(CheckResult::pass("ok"));
301        r.finish();
302        assert_eq!(r.overall_verdict(), Verdict::Pass);
303        assert!(r.passed());
304
305        let _ev = Evidence::numeric_int("count", 42);
306        let _opts = DiffOptions::default();
307        let _multi = MultiReport::new("c", "0.1.0");
308
309        // 0.9.2: also includes DurationRegression and SeverityChange.
310        let _dr: Option<DurationRegression> = None;
311        let _sc: Option<SeverityChange> = None;
312
313        // Sanity-check that Severity and Producer/Diff/etc. are in scope.
314        let _sev = Severity::Error;
315        fn _takes_producer(_p: &dyn Producer) {}
316        fn _takes_diff(_d: &Diff) {}
317    }
318
319    #[cfg(feature = "fixtures")]
320    #[test]
321    fn fixtures_module_is_available_with_feature() {
322        let _ = fixtures::TempProject::new();
323    }
324
325    #[cfg(feature = "bench")]
326    #[test]
327    fn bench_module_is_available_with_feature() {
328        let _ = bench::Benchmark::new("x");
329    }
330
331    #[test]
332    fn full_run_combines_zero_producers() {
333        let multi = full_run!("crate", "0.1.0";);
334        assert_eq!(multi.reports.len(), 0);
335        assert_eq!(multi.overall_verdict(), report::Verdict::Skip);
336    }
337
338    #[test]
339    fn full_run_combines_two_producers() {
340        struct OkProducer(&'static str);
341        impl report::Producer for OkProducer {
342            fn produce(&self) -> report::Report {
343                let mut r = report::Report::new("c", "0.1.0").with_producer(self.0);
344                r.push(report::CheckResult::pass("x"));
345                r.finish();
346                r
347            }
348        }
349        let multi = full_run!("c", "0.1.0"; OkProducer("a"), OkProducer("b"));
350        assert_eq!(multi.reports.len(), 2);
351        assert_eq!(multi.overall_verdict(), report::Verdict::Pass);
352    }
353
354    #[test]
355    fn full_run_propagates_failures() {
356        struct OkProducer;
357        impl report::Producer for OkProducer {
358            fn produce(&self) -> report::Report {
359                let mut r = report::Report::new("c", "0.1.0").with_producer("ok");
360                r.push(report::CheckResult::pass("x"));
361                r.finish();
362                r
363            }
364        }
365        struct FailProducer;
366        impl report::Producer for FailProducer {
367            fn produce(&self) -> report::Report {
368                let mut r = report::Report::new("c", "0.1.0").with_producer("fail");
369                r.push(report::CheckResult::fail("y", report::Severity::Error));
370                r.finish();
371                r
372            }
373        }
374        let multi = full_run!("c", "0.1.0"; OkProducer, FailProducer);
375        assert_eq!(multi.overall_verdict(), report::Verdict::Fail);
376    }
377
378    #[cfg(all(feature = "fixtures", feature = "bench"))]
379    #[test]
380    fn full_run_with_real_producers() {
381        // fixtures: a self-test of TempProject lifecycle.
382        let fixture_producer =
383            fixtures::FixtureProducer::new("temp_project_lifecycle", "0.1.0", || {
384                let _p = fixtures::TempProject::new()
385                    .with_file("README.md", "hi")
386                    .build()?;
387                Ok(())
388            });
389        // bench: a tiny benchmark with no baseline.
390        let bench_producer = bench::BenchProducer::new(
391            || {
392                let mut b = bench::Benchmark::new("hot");
393                for _ in 0..5 {
394                    b.iter(|| std::hint::black_box(1 + 1));
395                }
396                b.finish()
397            },
398            "0.1.0",
399            None,
400            bench::Threshold::regression_pct(20.0),
401        );
402        let multi = full_run!("crate", "0.1.0"; fixture_producer, bench_producer);
403        assert_eq!(multi.reports.len(), 2);
404    }
405
406    #[cfg(feature = "async")]
407    #[test]
408    fn async_full_run_compiles() {
409        // Compile-time check that async_full_run! expands cleanly.
410        // We don't drive the future here (no runtime in dev-deps), but
411        // compilation alone is meaningful: it catches macro-hygiene bugs.
412        async fn produce_a() -> report::Report {
413            let mut r = report::Report::new("c", "0.1.0").with_producer("a");
414            r.push(report::CheckResult::pass("x"));
415            r.finish();
416            r
417        }
418        let _fut = async_full_run!("c", "0.1.0"; produce_a(), produce_a());
419        // Drop the future without polling; compiles cleanly.
420    }
421}