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