cargo_deny/
test_utils.rs

1use crate::{
2    CheckCtx, PathBuf,
3    cfg::ValidationContext,
4    diag::{self, ErrorSink, FileId, Files, KrateSpans, PackChannel},
5};
6
7#[derive(Default, Clone)]
8pub struct KrateGather<'k> {
9    pub name: &'k str,
10    pub features: &'k [&'k str],
11    pub targets: &'k [&'k str],
12    pub all_features: bool,
13    pub no_default_features: bool,
14}
15
16impl<'k> KrateGather<'k> {
17    pub fn new(name: &'k str) -> Self {
18        Self {
19            name,
20            ..Default::default()
21        }
22    }
23
24    pub fn gather(self) -> crate::Krates {
25        let mut project_dir = crate::PathBuf::from("./tests/test_data");
26        project_dir.push(self.name);
27
28        let mut cmd = krates::Cmd::new();
29        cmd.current_dir(project_dir);
30
31        if self.all_features {
32            cmd.all_features();
33        }
34
35        if self.no_default_features {
36            cmd.no_default_features();
37        }
38
39        if !self.features.is_empty() {
40            cmd.features(self.features.iter().map(|f| (*f).to_owned()));
41        }
42
43        let mut kb = krates::Builder::new();
44
45        if !self.targets.is_empty() {
46            kb.include_targets(self.targets.iter().map(|t| (t, vec![])));
47        }
48
49        kb.build(cmd, krates::NoneFilter)
50            .expect("failed to build crate graph")
51    }
52}
53
54pub struct Config<C> {
55    pub deserialized: C,
56    pub config: String,
57}
58
59impl<C> Default for Config<C>
60where
61    C: Default,
62{
63    fn default() -> Self {
64        Self {
65            deserialized: C::default(),
66            config: "".to_owned(),
67        }
68    }
69}
70
71impl<C> Config<C>
72where
73    C: toml_span::DeserializeOwned,
74{
75    pub fn new(config: impl Into<String>) -> Self {
76        let config = config.into();
77        let mut val = toml_span::parse(&config).expect("failed to parse test config");
78        let deserialized = C::deserialize(&mut val).expect("failed to deserialize test config");
79        Self {
80            deserialized,
81            config,
82        }
83    }
84}
85
86impl<'de, C> From<&'de str> for Config<C>
87where
88    C: toml_span::DeserializeOwned,
89{
90    fn from(s: &'de str) -> Self {
91        Self::new(s)
92    }
93}
94
95impl<C> From<String> for Config<C>
96where
97    C: toml_span::DeserializeOwned + Default,
98{
99    fn from(s: String) -> Self {
100        Self::new(s)
101    }
102}
103
104pub struct ConfigData<T> {
105    pub config: T,
106    pub files: Files,
107    pub id: FileId,
108}
109
110impl<T> ConfigData<T> {
111    pub fn file(&self) -> &str {
112        self.files.source(self.id)
113    }
114}
115
116impl<T> ConfigData<T>
117where
118    T: toml_span::DeserializeOwned,
119{
120    pub fn load_str(name: impl Into<crate::PathBuf>, contents: impl Into<String>) -> Self {
121        let contents: String = contents.into();
122
123        let res = {
124            let mut cval = toml_span::parse(&contents).expect("failed to parse toml");
125            T::deserialize(&mut cval)
126        };
127
128        let mut files = Files::new();
129        let id = files.add(name, contents);
130
131        let config = match res {
132            Ok(v) => v,
133            Err(derr) => {
134                let diag_str = write_diagnostics(
135                    &files,
136                    derr.errors.into_iter().map(|err| err.to_diagnostic(id)),
137                );
138                panic!("failed to deserialize:\n---\n{diag_str}\n---");
139            }
140        };
141
142        ConfigData { config, files, id }
143    }
144
145    pub fn load(path: impl Into<PathBuf>) -> Self {
146        let path = path.into();
147        let contents = std::fs::read_to_string(&path).unwrap();
148
149        Self::load_str(path, contents)
150    }
151}
152
153impl<T> ConfigData<T> {
154    pub fn validate<IV, V>(mut self, conv: impl FnOnce(T) -> IV) -> V
155    where
156        IV: super::UnvalidatedConfig<ValidCfg = V>,
157    {
158        let uvc = conv(self.config);
159
160        let mut diagnostics = Vec::new();
161        let vcfg = uvc.validate(ValidationContext {
162            cfg_id: self.id,
163            files: &mut self.files,
164            diagnostics: &mut diagnostics,
165        });
166
167        if diagnostics.is_empty() {
168            vcfg
169        } else {
170            let diag_str = write_diagnostics(&self.files, diagnostics.into_iter());
171
172            panic!("failed to validate config:\n---\n{diag_str}\n---");
173        }
174    }
175
176    pub fn validate_with_diags<IV, V>(
177        mut self,
178        conv: impl FnOnce(T) -> IV,
179        on_diags: impl FnOnce(&Files, Vec<crate::diag::Diagnostic>),
180    ) -> V
181    where
182        IV: super::UnvalidatedConfig<ValidCfg = V>,
183    {
184        let uvc = conv(self.config);
185
186        let mut diagnostics = Vec::new();
187        let vcfg = uvc.validate(ValidationContext {
188            cfg_id: self.id,
189            files: &mut self.files,
190            diagnostics: &mut diagnostics,
191        });
192
193        on_diags(&self.files, diagnostics);
194        vcfg
195    }
196}
197
198pub(crate) fn write_diagnostics(
199    files: &Files,
200    errors: impl Iterator<Item = crate::diag::Diagnostic>,
201) -> String {
202    let mut s = codespan_reporting::term::termcolor::NoColor::new(Vec::new());
203    let config = crate::diag::codespan_config();
204
205    for diag in errors {
206        codespan_reporting::term::emit(&mut s, &config, files, &diag).unwrap();
207    }
208
209    String::from_utf8(s.into_inner()).unwrap()
210}
211
212#[inline]
213pub fn gather_diagnostics<C, VC, R>(
214    krates: &crate::Krates,
215    test_name: &str,
216    cfg: Config<C>,
217    runner: R,
218) -> Vec<serde_json::Value>
219where
220    C: crate::UnvalidatedConfig<ValidCfg = VC>,
221    VC: Send,
222    R: FnOnce(CheckCtx<'_, VC>, PackChannel) + Send,
223{
224    let ctx = setup(krates, test_name, cfg);
225    run_gather(ctx, runner)
226}
227
228pub struct GatherCtx<'k, VC> {
229    pub krates: &'k crate::Krates,
230    pub files: crate::diag::Files,
231    pub valid_cfg: VC,
232    pub spans: crate::diag::KrateSpans<'k>,
233}
234
235pub fn setup<'k, C, VC>(
236    krates: &'k crate::Krates,
237    test_name: &str,
238    cfg: Config<C>,
239) -> GatherCtx<'k, VC>
240where
241    C: crate::UnvalidatedConfig<ValidCfg = VC>,
242    VC: Send,
243{
244    let mut files = crate::diag::Files::new();
245    let spans = KrateSpans::synthesize(krates, test_name, &mut files);
246
247    let config = cfg.deserialized;
248    let cfg_id = files.add(format!("{test_name}.toml"), cfg.config);
249
250    let mut cfg_diags = Vec::new();
251    let valid_cfg = config.validate(crate::cfg::ValidationContext {
252        cfg_id,
253        files: &mut files,
254        diagnostics: &mut cfg_diags,
255    });
256
257    if cfg_diags
258        .iter()
259        .any(|d| d.severity >= crate::diag::Severity::Error)
260    {
261        panic!("encountered errors validating config: {cfg_diags:#?}");
262    }
263
264    GatherCtx {
265        krates,
266        files,
267        valid_cfg,
268        spans,
269    }
270}
271
272pub fn run_gather<VC, R>(ctx: GatherCtx<'_, VC>, runner: R) -> Vec<serde_json::Value>
273where
274    VC: Send,
275    R: FnOnce(CheckCtx<'_, VC>, PackChannel) + Send,
276{
277    let (tx, rx) = crossbeam::channel::unbounded();
278
279    let grapher = diag::InclusionGrapher::new(ctx.krates);
280
281    let (_, gathered) = rayon::join(
282        || {
283            let cctx = crate::CheckCtx {
284                krates: ctx.krates,
285                krate_spans: &ctx.spans,
286                cfg: ctx.valid_cfg,
287                serialize_extra: true,
288                colorize: false,
289                log_level: log::LevelFilter::Info,
290                files: &ctx.files,
291            };
292            runner(cctx, tx);
293        },
294        || {
295            let mut diagnostics = Vec::new();
296
297            let default = if std::env::var_os("CI").is_some() {
298                60
299            } else {
300                30
301            };
302
303            let timeout = std::env::var("CARGO_DENY_TEST_TIMEOUT_SECS")
304                .ok()
305                .and_then(|ts| ts.parse().ok())
306                .unwrap_or(default);
307            let timeout = std::time::Duration::from_secs(timeout);
308
309            let trx = crossbeam::channel::after(timeout);
310            loop {
311                crossbeam::select! {
312                    recv(rx) -> msg => {
313                        if let Ok(pack) = msg {
314                            diagnostics.extend(pack);
315                        } else {
316                            // Yay, the sender was dropped (i.e. check was finished)
317                            break;
318                        }
319                    }
320                    recv(trx) -> _ => {
321                        anyhow::bail!("Timed out after {timeout:?}");
322                    }
323                }
324            }
325
326            Ok(diagnostics)
327        },
328    );
329
330    gathered
331        .unwrap()
332        .into_iter()
333        .map(|d| diag::diag_to_json(d, &ctx.files, Some(&grapher)))
334        .collect()
335}
336
337#[macro_export]
338macro_rules! field_eq {
339    ($obj:expr, $field:expr, $expected:expr) => {
340        $obj.pointer($field) == Some(&serde_json::json!($expected))
341    };
342}
343
344#[macro_export]
345macro_rules! assert_field_eq {
346    ($obj:expr, $field:expr, $expected:expr) => {
347        assert_eq!($obj.pointer($field), Some(&serde_json::json!($expected)));
348    };
349}
350
351#[macro_export]
352macro_rules! func_name {
353    () => {{
354        fn f() {}
355        fn type_name_of<T>(_: T) -> &'static str {
356            std::any::type_name::<T>()
357        }
358        let name = type_name_of(f);
359        &name[..name.len() - 3]
360    }};
361}
362
363#[macro_export]
364macro_rules! overrides {
365    ($($code:expr => $severity:ident),* $(,)?) => {
366        {
367            let mut map = std::collections::BTreeMap::new();
368
369            $(map.insert($code, $crate::diag::Severity::$severity);)*
370
371            $crate::diag::DiagnosticOverrides {
372                code_overrides: map,
373                level_overrides: Vec::new(),
374            }
375        }
376    }
377}
378
379#[inline]
380pub fn gather_bans(
381    name: &str,
382    kg: KrateGather<'_>,
383    cfg: impl Into<Config<crate::bans::cfg::Config>>,
384) -> Vec<serde_json::Value> {
385    let krates = kg.gather();
386    let cfg = cfg.into();
387
388    gather_diagnostics::<crate::bans::cfg::Config, _, _>(&krates, name, cfg, |ctx, tx| {
389        crate::bans::check(ctx, None, tx);
390    })
391}
392
393#[inline]
394pub fn gather_bans_with_overrides(
395    name: &str,
396    kg: KrateGather<'_>,
397    cfg: impl Into<Config<crate::bans::cfg::Config>>,
398    overrides: diag::DiagnosticOverrides,
399) -> Vec<serde_json::Value> {
400    let krates = kg.gather();
401    let cfg = cfg.into();
402
403    gather_diagnostics::<crate::bans::cfg::Config, _, _>(&krates, name, cfg, |ctx, tx| {
404        crate::bans::check(
405            ctx,
406            None,
407            ErrorSink {
408                overrides: Some(std::sync::Arc::new(overrides)),
409                channel: tx,
410            },
411        );
412    })
413}