Skip to main content

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