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