necessist_core/
core.rs

1use crate::{
2    __ToConsoleString, Backup, Outcome, Rewriter, SourceFile, Span, WarnFlags, Warning, config,
3    framework::{self, Applicable, Postprocess, SourceFileSpanTestMap, SpanKind, ToImplementation},
4    note, source_warn, sqlite, util, warn,
5};
6use ansi_term::Style;
7use anyhow::{Context as _, Result, anyhow, bail, ensure};
8use heck::ToKebabCase;
9use indexmap::IndexSet;
10use indicatif::ProgressBar;
11use itertools::{PeekNth, peek_nth};
12use log::debug;
13use once_cell::sync::OnceCell;
14use std::{
15    cell::RefCell,
16    collections::BTreeMap,
17    env::{current_dir, var},
18    fmt::Display,
19    io::{IsTerminal, Write},
20    iter::Peekable,
21    path::{Path, PathBuf},
22    process::{Command, ExitStatus as StdExitStatus, Stdio},
23    rc::Rc,
24    sync::atomic::{AtomicBool, Ordering},
25    time::Duration,
26};
27use strum::IntoEnumIterator;
28use subprocess::{Exec, ExitStatus};
29
30const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
31
32static CTRLC: AtomicBool = AtomicBool::new(false);
33
34#[derive(Clone)]
35pub(crate) struct Removal {
36    pub span: Span,
37    pub text: String,
38    pub outcome: Outcome,
39}
40
41#[derive(Debug)]
42enum MismatchKind {
43    Missing,
44    Unexpected,
45}
46
47impl Display for MismatchKind {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "{}", format!("{self:?}").to_kebab_case())
50    }
51}
52
53struct Mismatch {
54    kind: MismatchKind,
55    removal: Removal,
56}
57
58struct Context<'a> {
59    opts: Necessist,
60    root: Rc<PathBuf>,
61    println: &'a dyn Fn(&dyn AsRef<str>),
62    backend: Box<dyn framework::Interface>,
63    progress: Option<&'a ProgressBar>,
64}
65
66impl Context<'_> {
67    fn light(&self) -> LightContext<'_> {
68        LightContext {
69            opts: &self.opts,
70            root: &self.root,
71            println: self.println,
72        }
73    }
74}
75
76pub struct LightContext<'a> {
77    pub opts: &'a Necessist,
78    pub root: &'a Rc<PathBuf>,
79    pub println: &'a dyn Fn(&dyn AsRef<str>),
80}
81
82#[allow(clippy::struct_excessive_bools)]
83#[derive(Clone, Default)]
84pub struct Necessist {
85    pub allow: Vec<Warning>,
86    pub default_config: bool,
87    pub deny: Vec<Warning>,
88    pub dump: bool,
89    pub dump_candidate_counts: bool,
90    pub dump_candidates: bool,
91    pub no_local_functions: bool,
92    pub no_sqlite: bool,
93    pub quiet: bool,
94    pub reset: bool,
95    pub resume: bool,
96    pub root: Option<PathBuf>,
97    pub timeout: Option<u64>,
98    pub verbose: bool,
99    pub source_files: Vec<PathBuf>,
100    pub args: Vec<String>,
101}
102
103/// Necessist's main entrypoint.
104// smoelius: The reason `framework` is not included as a field in `Necessist` is to avoid having
105// to parameterize every function that takes a `Necessist` as an argument.
106pub fn necessist<Identifier: Applicable + Display + IntoEnumIterator + ToImplementation>(
107    opts: &Necessist,
108    framework: framework::Auto<Identifier>,
109) -> Result<()> {
110    let opts = opts.clone();
111
112    process_options(&opts)?;
113
114    let root = opts
115        .root
116        .as_ref()
117        .map_or_else(current_dir, dunce::canonicalize)
118        .map(Rc::new)?;
119
120    #[cfg(feature = "lock_root")]
121    let _file: std::fs::File = lock_root(&root)?;
122
123    let mut context = LightContext {
124        opts: &opts,
125        root: &root,
126        println: &|_| {},
127    };
128
129    let println = |msg: &dyn AsRef<str>| {
130        println!("{}", msg.as_ref());
131    };
132
133    if !opts.quiet {
134        context.println = &println;
135    }
136
137    if opts.no_local_functions {
138        warn(
139            &context,
140            Warning::OptionDeprecated,
141            "--no-local-functions is now the default; hence, this option is deprecated",
142            WarnFlags::empty(),
143        )?;
144    }
145
146    let Some((backend, n_spans, source_file_span_test_map)) = prepare(&context, framework)? else {
147        return Ok(());
148    };
149
150    let mut context = Context {
151        opts,
152        root,
153        println: &|_| {},
154        backend,
155        progress: None,
156    };
157
158    if !context.opts.quiet {
159        context.println = &println;
160    }
161
162    let progress =
163        if var("RUST_LOG").is_err() && !context.opts.quiet && std::io::stdout().is_terminal() {
164            Some(ProgressBar::new(n_spans as u64))
165        } else {
166            None
167        };
168
169    let progress_println = |msg: &dyn AsRef<str>| {
170        // SAFETY: This closure should only be called when `progress` is `Some`.
171        // The unwrap is intentional - we want to panic if this invariant is violated.
172        #[allow(clippy::unwrap_used)]
173        progress.as_ref().unwrap().println(msg);
174    };
175
176    // Only set this function as the printer when `progress` exists
177    if progress.is_some() {
178        context.println = &progress_println;
179        context.progress = progress.as_ref();
180    }
181
182    run(context, source_file_span_test_map)
183}
184
185#[allow(clippy::type_complexity)]
186#[cfg_attr(dylint_lib = "supplementary", allow(commented_out_code))]
187fn prepare<Identifier: Applicable + Display + IntoEnumIterator + ToImplementation>(
188    context: &LightContext,
189    framework: framework::Auto<Identifier>,
190) -> Result<Option<(Box<dyn framework::Interface>, usize, SourceFileSpanTestMap)>> {
191    if context.opts.default_config {
192        default_config(context, context.root)?;
193        return Ok(None);
194    }
195
196    let config = config::Toml::read(context, context.root)?;
197
198    if context.opts.dump {
199        let past_removals = past_removals_init_lazy(context)?;
200        dump(context, &past_removals);
201        return Ok(None);
202    }
203
204    let mut backend = backend_for_framework(context, framework)?;
205
206    let paths = canonicalize_source_files(context)?;
207
208    let (n_tests, source_file_span_test_map) = backend.parse(
209        context,
210        &config,
211        &paths.iter().map(AsRef::as_ref).collect::<Vec<_>>(),
212    )?;
213
214    let n_spans = source_file_span_test_map
215        .values()
216        .map(|span_test_maps| {
217            span_test_maps
218                .statement
219                .values()
220                .map(IndexSet::len)
221                .sum::<usize>()
222                + span_test_maps
223                    .method_call
224                    .values()
225                    .map(IndexSet::len)
226                    .sum::<usize>()
227        })
228        .sum();
229
230    if context.opts.dump_candidates {
231        dump_candidates(context, &source_file_span_test_map)?;
232        return Ok(None);
233    }
234
235    if context.opts.dump_candidate_counts {
236        dump_candidate_counts(context, &source_file_span_test_map);
237        return Ok(None);
238    }
239
240    // smoelius: Curious. The code used to look like:
241    // ```
242    //     (context.println)({
243    //         let n_source_files = source_file_span_test_map.keys().len();
244    //         &format!(
245    //             ...
246    //         )
247    //     });
248    // ```
249    // But with Rust Edition 2024, that would cause the compiler to say:
250    // ```
251    // error[E0716]: temporary value dropped while borrowed
252    //    --> core/src/core.rs:233:10
253    //     |
254    // 233 |            &format!(
255    //     |   _________-^
256    //     |  |__________|
257    // 234 | ||             "{} candidates in {} test{} in {} source file{}",
258    // 235 | ||             n_spans,
259    // 236 | ||             n_tests,
260    // ...   ||
261    // 239 | ||             if n_source_files == 1 { "" } else { "s" }
262    // 240 | ||         )
263    //     | ||         ^
264    //     | ||         |
265    //     | ||_________temporary value is freed at the end of this statement
266    //     |  |_________creates a temporary value which is freed while still in use
267    //     |            borrow later used here
268    //     |
269    //     = note: consider using a `let` binding to create a longer lived value
270    //     = note: this error originates in the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info)
271    // ```
272    let n_source_files = source_file_span_test_map.keys().len();
273    (context.println)(&format!(
274        "{} candidates in {} test{} in {} source file{}",
275        n_spans,
276        n_tests,
277        if n_tests == 1 { "" } else { "s" },
278        n_source_files,
279        if n_source_files == 1 { "" } else { "s" }
280    ));
281
282    Ok(Some((backend, n_spans, source_file_span_test_map)))
283}
284
285fn run(mut context: Context, source_file_span_test_map: SourceFileSpanTestMap) -> Result<()> {
286    ctrlc::set_handler(|| CTRLC.store(true, Ordering::SeqCst))?;
287
288    let past_removals = past_removals_init_lazy(&context.light())?;
289
290    let mut past_removal_iter = past_removals.into_iter().peekable();
291
292    for (source_file, span_test_maps) in source_file_span_test_map {
293        let mut span_test_iter = peek_nth(span_test_maps.iter());
294
295        let (mismatch, n) = skip_past_removals(&mut span_test_iter, &mut past_removal_iter);
296
297        update_progress(&context, mismatch, n)?;
298
299        if span_test_iter.peek().is_none() {
300            continue;
301        }
302
303        (context.println)(&format!(
304            "{}: dry running",
305            util::strip_current_dir(&source_file).to_string_lossy()
306        ));
307
308        let result = context.backend.dry_run(&context.light(), &source_file);
309
310        if let Err(error) = &result {
311            source_warn(
312                &context.light(),
313                Warning::DryRunFailed,
314                &source_file,
315                &format!("dry run failed: {error:?}"),
316                WarnFlags::empty(),
317            )?;
318        }
319
320        if CTRLC.load(Ordering::SeqCst) {
321            bail!("Ctrl-C detected");
322        }
323
324        if result.is_err() {
325            let n = skip_present_spans(&context, span_test_iter)?;
326            update_progress(&context, None, n)?;
327            continue;
328        }
329
330        (context.println)(&format!(
331            "{}: mutilating",
332            util::strip_current_dir(&source_file).to_string_lossy()
333        ));
334
335        let mut instrumentation_backup =
336            instrument_statements(&context, &source_file, &mut span_test_iter)?;
337
338        loop {
339            let (mismatch, n) = skip_past_removals(&mut span_test_iter, &mut past_removal_iter);
340
341            update_progress(&context, mismatch, n)?;
342
343            let Some((span, span_kind, test_names)) = span_test_iter.next() else {
344                break;
345            };
346
347            if span_kind != SpanKind::Statement {
348                drop(instrumentation_backup.take());
349            }
350
351            let text = span.source_text()?;
352
353            let explicit_removal =
354                instrumentation_backup.is_none() || span_kind != SpanKind::Statement;
355
356            let _explicit_backup = if explicit_removal {
357                let (_, explicit_backup) = span.remove()?;
358                Some(explicit_backup)
359            } else {
360                None
361            };
362
363            let outcome =
364                test_names
365                    .into_iter()
366                    .try_fold(Some(Outcome::Passed), |outcome, test_name| {
367                        if outcome != Some(Outcome::Passed) {
368                            return Ok(outcome);
369                        }
370
371                        if let Some((exec, postprocess)) =
372                            context.backend.exec(&context.light(), test_name, span)?
373                        {
374                            // smoelius: Even if the removal is explicit (i.e., not with
375                            // instrumentation), it doesn't hurt to set `NECESSIST_REMOVAL`.
376                            let exec = exec.env("NECESSIST_REMOVAL", span.id());
377
378                            perform_exec(&context, exec, postprocess)
379                        } else {
380                            assert!(
381                                explicit_removal,
382                                "Instrumentation failed to build after it was verified to"
383                            );
384
385                            Ok(Some(Outcome::Nonbuildable))
386                        }
387                    })?;
388
389            if CTRLC.load(Ordering::SeqCst) {
390                bail!("Ctrl-C detected");
391            }
392
393            if let Some(outcome) = outcome {
394                emit(&mut context, span, &text, outcome)?;
395            }
396
397            update_progress(&context, None, 1)?;
398        }
399    }
400
401    context.progress.map(ProgressBar::finish);
402
403    Ok(())
404}
405
406macro_rules! incompatible {
407    ($opts:ident, $x:ident, $y:ident) => {
408        ensure!(
409            !($opts.$x && $opts.$y),
410            "--{} and --{} are incompatible",
411            stringify!($x).to_kebab_case(),
412            stringify!($y).to_kebab_case()
413        );
414    };
415}
416
417fn process_options(opts: &Necessist) -> Result<()> {
418    // smoelius: This list of incompatibilities is not exhaustive.
419    incompatible!(opts, dump, quiet);
420    incompatible!(opts, dump, reset);
421    incompatible!(opts, dump, resume);
422    incompatible!(opts, dump, no_sqlite);
423    incompatible!(opts, quiet, verbose);
424    incompatible!(opts, reset, no_sqlite);
425    incompatible!(opts, resume, no_sqlite);
426
427    Ok(())
428}
429
430#[cfg(feature = "lock_root")]
431fn lock_root(root: &Path) -> Result<std::fs::File> {
432    if enabled("TRYCMD") {
433        crate::flock::lock_path(root)
434    } else {
435        crate::flock::try_lock_path(root)
436    }
437    .with_context(|| format!("Failed to lock `{}`", root.display()))
438}
439
440#[cfg(feature = "lock_root")]
441fn enabled(key: &str) -> bool {
442    var(key).is_ok_and(|value| value != "0")
443}
444
445fn default_config(_context: &LightContext, root: &Path) -> Result<()> {
446    let path_buf = root.join("necessist.toml");
447
448    if path_buf.try_exists()? {
449        bail!("A configuration file already exists at {:?}", path_buf);
450    }
451
452    let toml = toml::to_string(&config::Toml::default())?;
453
454    std::fs::write(path_buf, toml).map_err(Into::into)
455}
456
457fn dump(context: &LightContext, removals: &[Removal]) {
458    let mut other_than_passed = false;
459    for removal in removals {
460        emit_to_console(context, removal);
461        other_than_passed |= removal.outcome != Outcome::Passed;
462    }
463
464    if !context.opts.verbose && other_than_passed {
465        note(context, "More output would be produced with --verbose");
466    }
467}
468
469fn backend_for_framework<Identifier: Applicable + Display + IntoEnumIterator + ToImplementation>(
470    context: &LightContext,
471    identifier: framework::Auto<Identifier>,
472) -> Result<Box<dyn framework::Interface>> {
473    let implementation = identifier.to_implementation(context)?;
474
475    drop(identifier);
476
477    implementation.ok_or_else(|| anyhow!("Found no applicable frameworks"))
478}
479
480fn canonicalize_source_files(context: &LightContext) -> Result<Vec<PathBuf>> {
481    context
482        .opts
483        .source_files
484        .iter()
485        .map(|path| {
486            let path_buf = dunce::canonicalize(path)
487                .with_context(|| format!("Failed to canonicalize `{}`", path.display()))?;
488            ensure!(
489                path_buf.starts_with(context.root.as_path()),
490                "{:?} is not in {:?}",
491                path_buf,
492                context.root
493            );
494            Ok(path_buf)
495        })
496        .collect::<Result<Vec<_>>>()
497}
498
499#[must_use]
500fn skip_past_removals<'a, I, J>(
501    span_test_iter: &mut PeekNth<I>,
502    removal_iter: &mut Peekable<J>,
503) -> (Option<Mismatch>, usize)
504where
505    I: Iterator<Item = (&'a Span, SpanKind, &'a IndexSet<String>)>,
506    J: Iterator<Item = Removal>,
507{
508    let mut mismatch = None;
509    let mut n = 0;
510    while let Some(&(span, _, _)) = span_test_iter.peek() {
511        let Some(removal) = removal_iter.peek() else {
512            break;
513        };
514        match span.cmp(&removal.span) {
515            std::cmp::Ordering::Less => {
516                mismatch = Some(Mismatch {
517                    kind: MismatchKind::Unexpected,
518                    removal: removal.clone(),
519                });
520                break;
521            }
522            std::cmp::Ordering::Equal => {
523                let _: Option<(&Span, _, _)> = span_test_iter.next();
524                let _removal: Option<Removal> = removal_iter.next();
525                n += 1;
526            }
527            std::cmp::Ordering::Greater => {
528                if mismatch.is_none() {
529                    mismatch = Some(Mismatch {
530                        kind: MismatchKind::Missing,
531                        removal: removal.clone(),
532                    });
533                }
534                let _removal: Option<Removal> = removal_iter.next();
535            }
536        }
537    }
538
539    (mismatch, n)
540}
541
542fn skip_present_spans<'a>(
543    context: &Context,
544    span_test_iter: impl IntoIterator<Item = (&'a Span, SpanKind, &'a IndexSet<String>)>,
545) -> Result<usize> {
546    let mut n = 0;
547
548    let sqlite = sqlite_init_lazy(&context.light())?;
549
550    for (span, _, _) in span_test_iter {
551        if let Some(sqlite) = sqlite.borrow_mut().as_mut() {
552            let text = span.source_text()?;
553            let removal = Removal {
554                span: span.clone(),
555                text,
556                outcome: Outcome::Skipped,
557            };
558            sqlite::insert(sqlite, &removal)?;
559        }
560        n += 1;
561    }
562
563    Ok(n)
564}
565
566fn update_progress(context: &Context, mismatch: Option<Mismatch>, n: usize) -> Result<()> {
567    if let Some(Mismatch {
568        kind,
569        removal: Removal { span, text, .. },
570    }) = mismatch
571    {
572        warn(
573            &context.light(),
574            Warning::FilesChanged,
575            &format!(
576                "\
577Configuration or source files have changed since necessist.db was created; the following entry is \
578                 {kind}:
579    {}: `{}`",
580                span.to_console_string(),
581                text.replace('\r', ""),
582            ),
583            WarnFlags::ONCE,
584        )?;
585    }
586
587    if let Some(bar) = context.progress {
588        bar.inc(n as u64);
589    }
590
591    Ok(())
592}
593
594fn dump_candidates(
595    context: &LightContext,
596    source_file_span_test_map: &SourceFileSpanTestMap,
597) -> Result<()> {
598    for span in source_file_span_test_map
599        .values()
600        .flat_map(|span_test_maps| {
601            span_test_maps
602                .statement
603                .keys()
604                .chain(span_test_maps.method_call.keys())
605        })
606    {
607        let text = span.source_text()?;
608
609        (context.println)(&format!(
610            "{}: `{}`",
611            span.to_console_string(),
612            text.replace('\r', "")
613        ));
614    }
615
616    Ok(())
617}
618
619fn dump_candidate_counts(
620    context: &LightContext,
621    source_file_span_test_map: &SourceFileSpanTestMap,
622) {
623    let mut candidate_counts = source_file_span_test_map
624        .iter()
625        .map(|(source_file, span_test_maps)| {
626            (
627                span_test_maps.statement.keys().count() + span_test_maps.method_call.keys().count(),
628                source_file,
629            )
630        })
631        .collect::<Vec<_>>();
632
633    candidate_counts.sort();
634
635    let Some(width) = candidate_counts
636        .iter()
637        .map(|(count, _)| count.to_string().len())
638        .max()
639    else {
640        return;
641    };
642
643    for (count, source_file) in candidate_counts {
644        (context.println)(&format!(
645            "{count:width$} {}",
646            source_file.to_console_string(),
647        ));
648    }
649}
650
651fn instrument_statements<'a, I>(
652    context: &Context,
653    source_file: &SourceFile,
654    span_test_iter: &mut PeekNth<I>,
655) -> Result<Option<Backup>>
656where
657    I: Iterator<Item = (&'a Span, SpanKind, &'a IndexSet<String>)>,
658{
659    let backup = Backup::new(source_file)?;
660
661    let mut rewriter =
662        Rewriter::with_offset_calculator(source_file.contents(), source_file.offset_calculator());
663
664    let n_instrumentable_statements = count_instrumentable_statements(span_test_iter);
665
666    context.backend.instrument_source_file(
667        &context.light(),
668        &mut rewriter,
669        source_file,
670        n_instrumentable_statements,
671    )?;
672
673    let mut i_span = 0;
674    let mut insertion_map = BTreeMap::<_, Vec<_>>::new();
675    // smoelius: Do not advance the underlying iterator while instrumenting. This way, if a
676    // statement cannot be removed with instrumentation, it will be removed explicitly.
677    while let Some((span, SpanKind::Statement, _)) = span_test_iter.peek_nth(i_span) {
678        let (prefix, suffix) = context.backend.statement_prefix_and_suffix(span)?;
679        let insertions = insertion_map.entry(span.start()).or_default();
680        insertions.push(prefix);
681        let insertions = insertion_map.entry(span.end()).or_default();
682        insertions.push(suffix);
683        i_span += 1;
684    }
685
686    assert_eq!(n_instrumentable_statements, i_span);
687
688    for (line_column, insertions) in insertion_map {
689        for insertion in insertions {
690            source_file.insert(&mut rewriter, line_column, &insertion);
691        }
692    }
693
694    let mut file = std::fs::OpenOptions::new()
695        .truncate(true)
696        .write(true)
697        .open(source_file)?;
698    file.write_all(rewriter.contents().as_bytes())?;
699    drop(file);
700
701    let result = context
702        .backend
703        .build_source_file(&context.light(), source_file);
704    if let Err(error) = result {
705        warn(
706            &context.light(),
707            Warning::InstrumentationNonbuildable,
708            &format!(
709                "Instrumentation caused `{}` to be nonbuildable: {error:?}",
710                source_file.to_console_string(),
711            ),
712            WarnFlags::empty(),
713        )?;
714        return Ok(None);
715    }
716
717    Ok(Some(backup))
718}
719
720fn count_instrumentable_statements<'a, I>(span_test_iter: &mut PeekNth<I>) -> usize
721where
722    I: Iterator<Item = (&'a Span, SpanKind, &'a IndexSet<String>)>,
723{
724    let mut n_instrumentable_statements = 0;
725    while matches!(
726        span_test_iter.peek_nth(n_instrumentable_statements),
727        Some((_, SpanKind::Statement, _))
728    ) {
729        n_instrumentable_statements += 1;
730    }
731    n_instrumentable_statements
732}
733
734fn perform_exec(
735    context: &Context,
736    exec: Exec,
737    postprocess: Option<Box<Postprocess>>,
738) -> Result<Option<Outcome>> {
739    debug!("{exec:?}");
740
741    #[cfg(all(feature = "limit_threads", unix))]
742    let nprocs_prev = rlimit::set_soft_rlimit(
743        rlimit::Resource::NPROC,
744        *rlimit::NPROC_INIT + rlimit::NPROC_ALLOWANCE,
745    )?;
746
747    let mut popen = exec.popen()?;
748    let status = if let Some(dur) = timeout(&context.opts) {
749        popen.wait_timeout(dur)?
750    } else {
751        popen.wait().map(Option::Some)?
752    };
753
754    #[cfg(all(feature = "limit_threads", unix))]
755    rlimit::set_soft_rlimit(rlimit::Resource::NPROC, nprocs_prev)?;
756
757    if status.is_some() {
758        if let Some(postprocess) = postprocess
759            && !postprocess(&context.light(), popen)?
760        {
761            return Ok(None);
762        }
763    } else {
764        let pid = popen.pid().ok_or_else(|| anyhow!("Failed to get pid"))?;
765        transitive_kill(pid)?;
766        let _: ExitStatus = popen.wait()?;
767    }
768
769    let Some(status) = status else {
770        return Ok(Some(Outcome::TimedOut));
771    };
772
773    Ok(Some(if status.success() {
774        Outcome::Passed
775    } else {
776        Outcome::Failed
777    }))
778}
779
780#[cfg_attr(dylint_lib = "general", allow(non_local_effect_before_error_return))]
781fn emit(context: &mut Context, span: &Span, text: &str, outcome: Outcome) -> Result<()> {
782    let removal = Removal {
783        span: span.clone(),
784        text: text.to_owned(),
785        outcome,
786    };
787
788    let sqlite = sqlite_init_lazy(&context.light())?;
789
790    if let Some(sqlite) = sqlite.borrow_mut().as_mut() {
791        sqlite::insert(sqlite, &removal)?;
792    }
793
794    emit_to_console(&context.light(), &removal);
795
796    Ok(())
797}
798
799fn emit_to_console(context: &LightContext, removal: &Removal) {
800    let Removal {
801        span,
802        text,
803        outcome,
804    } = removal;
805
806    if !context.opts.quiet && (context.opts.verbose || *outcome == Outcome::Passed) {
807        let msg = format!(
808            "{}: `{}` {}",
809            span.to_console_string(),
810            text.replace('\r', ""),
811            if std::io::stdout().is_terminal() {
812                outcome.style().bold()
813            } else {
814                Style::default()
815            }
816            .paint(outcome.to_string())
817        );
818        (context.println)(&msg);
819    }
820}
821
822fn sqlite_init_lazy(context: &LightContext) -> Result<Rc<RefCell<Option<sqlite::Sqlite>>>> {
823    let (sqlite, _) = sqlite_and_past_removals_init_lazy(context)?;
824    Ok(sqlite)
825}
826
827fn past_removals_init_lazy(context: &LightContext) -> Result<Vec<Removal>> {
828    let (_, past_removals) = sqlite_and_past_removals_init_lazy(context)?;
829    Ok(past_removals.take())
830}
831
832thread_local! {
833    #[allow(clippy::type_complexity)]
834    static SQLITE_AND_PAST_REMOVALS: OnceCell<(
835        Rc<RefCell<Option<sqlite::Sqlite>>>,
836        Rc<RefCell<Vec<Removal>>>,
837    )> = const { OnceCell::new() };
838}
839
840#[allow(clippy::type_complexity)]
841fn sqlite_and_past_removals_init_lazy(
842    context: &LightContext,
843) -> Result<(
844    Rc<RefCell<Option<sqlite::Sqlite>>>,
845    Rc<RefCell<Vec<Removal>>>,
846)> {
847    SQLITE_AND_PAST_REMOVALS.with(|sqlite_and_past_removals| {
848        sqlite_and_past_removals
849            .get_or_try_init(|| {
850                if context.opts.no_sqlite {
851                    Ok((
852                        Rc::new(RefCell::new(None)),
853                        Rc::new(RefCell::new(Vec::new())),
854                    ))
855                } else {
856                    let (sqlite, mut past_removals) = sqlite::init(
857                        context,
858                        context.root,
859                        context.opts.dump,
860                        context.opts.reset,
861                        context.opts.resume,
862                    )?;
863                    past_removals.sort_by(|left, right| left.span.cmp(&right.span));
864                    Ok((
865                        Rc::new(RefCell::new(Some(sqlite))),
866                        Rc::new(RefCell::new(past_removals)),
867                    ))
868                }
869            })
870            .cloned()
871    })
872}
873
874#[allow(clippy::module_name_repetitions)]
875#[cfg(all(feature = "limit_threads", unix))]
876mod rlimit {
877    use anyhow::Result;
878    pub use rlimit::Resource;
879    use rlimit::{getrlimit, setrlimit};
880    use std::{process::Command, sync::LazyLock};
881
882    #[allow(clippy::unwrap_used)]
883    pub static NPROC_INIT: LazyLock<u64> = LazyLock::new(|| {
884        let output = Command::new("ps").arg("-eL").output().unwrap();
885        let stdout = std::str::from_utf8(&output.stdout).unwrap();
886        stdout.lines().count().try_into().unwrap()
887    });
888
889    // smoelius: Limit the number of threads that a test can allocate to approximately 1024 (an
890    // arbitrary choice).
891    //
892    // The limit is not strict for the following reason. `NPROC_INIT` counts the number of threads
893    // *started by any user*. But `setrlimit` (used to enforce the limit) applies to just the
894    // current user. So by setting the limit to `NPROC_INIT + NPROC_ALLOWANCE`, the number of
895    // threads the test can allocate is actually 1024 plus the number of threads started by other
896    // users.
897    pub const NPROC_ALLOWANCE: u64 = 1024;
898
899    pub fn set_soft_rlimit(resource: Resource, limit: u64) -> Result<u64> {
900        let (soft, hard) = getrlimit(resource)?;
901        setrlimit(Resource::NPROC, std::cmp::min(hard, limit), hard)?;
902        Ok(soft)
903    }
904}
905
906fn timeout(opts: &Necessist) -> Option<Duration> {
907    match opts.timeout {
908        None => Some(DEFAULT_TIMEOUT),
909        Some(0) => None,
910        Some(secs) => Some(Duration::from_secs(secs)),
911    }
912}
913
914#[cfg_attr(dylint_lib = "supplementary", allow(commented_out_code))]
915fn transitive_kill(pid: u32) -> Result<()> {
916    let mut pids = vec![(pid, false)];
917
918    while let Some((pid, visited)) = pids.pop() {
919        if visited {
920            let _status: StdExitStatus = kill()
921                .arg(pid.to_string())
922                .stdout(Stdio::null())
923                .stderr(Stdio::null())
924                .status()?;
925            // smoelius: The process may have already exited.
926            // ensure!(status.success());
927        } else {
928            pids.push((pid, true));
929
930            for line in child_processes(pid)? {
931                let pid = line
932                    .parse::<u32>()
933                    .with_context(|| format!("failed to parse `{line}`"))?;
934                pids.push((pid, false));
935            }
936        }
937    }
938
939    Ok(())
940}
941
942#[cfg(not(windows))]
943fn kill() -> Command {
944    Command::new("kill")
945}
946
947#[cfg(windows)]
948fn kill() -> Command {
949    let mut command = Command::new("taskkill");
950    command.args(["/f", "/pid"]);
951    command
952}
953
954#[cfg(not(windows))]
955fn child_processes(pid: u32) -> Result<Vec<String>> {
956    let output = Command::new("pgrep")
957        .args(["-P", &pid.to_string()])
958        .output()?;
959    let stdout = String::from_utf8(output.stdout)?;
960    Ok(stdout.lines().map(ToOwned::to_owned).collect())
961}
962
963#[cfg(windows)]
964fn child_processes(pid: u32) -> Result<Vec<String>> {
965    let output = Command::new("wmic")
966        .args([
967            "process",
968            "where",
969            &format!("ParentProcessId={pid}"),
970            "get",
971            "ProcessId",
972        ])
973        .output()?;
974    let stdout = String::from_utf8(output.stdout)?;
975    Ok(stdout
976        .lines()
977        .map(str::trim_end)
978        .filter(|line| !line.is_empty())
979        .skip(1)
980        .map(ToOwned::to_owned)
981        .collect())
982}