1use std::{
4 collections::HashMap,
5 sync::{
6 Arc,
7 atomic::{AtomicBool, AtomicUsize, Ordering},
8 },
9 time::{Duration, Instant},
10};
11
12use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
13use parking_lot::Mutex;
14use yansi::Paint;
15
16use crate::mutation::mutant::{Mutant, MutationResult};
17
18#[derive(Debug, Default, Clone, Copy)]
20struct LiveCounts {
21 killed: usize,
22 survived: usize,
23 invalid: usize,
24 skipped: usize,
25 timed_out: usize,
26}
27
28impl LiveCounts {
29 const fn record(&mut self, result: &MutationResult) {
30 match result {
31 MutationResult::Dead => self.killed += 1,
32 MutationResult::Alive => self.survived += 1,
33 MutationResult::Invalid => self.invalid += 1,
34 MutationResult::Skipped => self.skipped += 1,
35 MutationResult::TimedOut => self.timed_out += 1,
36 }
37 }
38}
39
40#[derive(Debug)]
44struct ActiveMutant {
45 pb: ProgressBar,
46 started_at: Instant,
47}
48
49#[derive(Debug)]
51pub struct MutationProgressState {
52 multi: MultiProgress,
53 overall_progress: ProgressBar,
54 active_mutants: HashMap<String, ActiveMutant>,
57 counts: LiveCounts,
59 timeout_secs: Option<u32>,
61 num_workers: usize,
63}
64
65impl MutationProgressState {
66 pub fn new(total_mutants: usize, num_workers: usize) -> Self {
67 Self::with_timeout(total_mutants, num_workers, None)
68 }
69
70 pub fn with_timeout(
71 total_mutants: usize,
72 num_workers: usize,
73 timeout_secs: Option<u32>,
74 ) -> Self {
75 let multi = MultiProgress::new();
76
77 let overall_progress = multi.add(ProgressBar::new(total_mutants as u64));
80 overall_progress.set_style(
81 ProgressStyle::with_template(
82 "{bar:40.cyan/blue} {pos:>4}/{len:4} mutants ({prefix} jobs) [{elapsed_precise}] {wide_msg}",
83 )
84 .unwrap()
85 .progress_chars("##-"),
86 );
87 overall_progress.set_prefix(num_workers.to_string());
88 overall_progress.enable_steady_tick(Duration::from_millis(100));
89
90 Self {
91 multi,
92 overall_progress,
93 active_mutants: HashMap::with_capacity(num_workers),
94 counts: LiveCounts::default(),
95 timeout_secs,
96 num_workers,
97 }
98 }
99
100 pub fn set_current_file(&mut self, file: &str) {
103 let msg = self.format_message(file);
105 self.overall_progress.set_message(msg);
106 }
107
108 fn format_message(&self, file: &str) -> String {
109 let counts = self.counts;
110 let timeout_suffix = match self.timeout_secs {
111 Some(t) => format!(" · timeout {t}s/mutant"),
112 None => String::new(),
113 };
114 format!(
115 "k:{} s:{} i:{} t:{} sk:{}{} · {}",
116 counts.killed,
117 counts.survived,
118 counts.invalid,
119 counts.timed_out,
120 counts.skipped,
121 timeout_suffix,
122 file,
123 )
124 }
125
126 fn refresh_message(&self) {
127 let current = self.overall_progress.message();
129 let file = current.rsplit(" · ").next().unwrap_or("");
130 self.overall_progress.set_message(self.format_message(file));
131 }
132
133 fn mutant_key(mutant: &Mutant) -> String {
135 format!(
136 "{}:{}-{}:{}",
137 mutant.path.display(),
138 mutant.span.lo().0,
139 mutant.span.hi().0,
140 mutant.mutation,
141 )
142 }
143
144 pub fn add_mutant_progress(&mut self, mutant: &Mutant) {
146 let pb = self.multi.add(ProgressBar::new_spinner());
147 pb.set_style(
148 ProgressStyle::with_template(" {spinner} {wide_msg}").unwrap().tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
149 );
150 pb.enable_steady_tick(Duration::from_millis(100));
151
152 let display = format!(
153 "line {}: `{}` → `{}`",
154 mutant.line_number,
155 truncate_str(&mutant.original, 40),
156 truncate_str(&mutant.mutation.to_string(), 40),
157 );
158 pb.set_message(display);
159
160 self.active_mutants
161 .insert(Self::mutant_key(mutant), ActiveMutant { pb, started_at: Instant::now() });
162 }
163
164 pub fn complete_mutant(&mut self, mutant: &Mutant, result: &MutationResult) {
167 self.counts.record(result);
168 self.overall_progress.inc(1);
169
170 let elapsed = self
171 .active_mutants
172 .remove(&Self::mutant_key(mutant))
173 .map(|am| {
174 let el = am.started_at.elapsed();
175 am.pb.finish_and_clear();
176 el
177 })
178 .unwrap_or_default();
179
180 let raw_label = format!("{:9}", result.label());
185 let label = match result {
186 MutationResult::Dead => Paint::green(&raw_label).bold().to_string(),
187 MutationResult::Alive => Paint::red(&raw_label).bold().to_string(),
188 MutationResult::TimedOut => Paint::yellow(&raw_label).bold().to_string(),
189 MutationResult::Invalid | MutationResult::Skipped => {
190 self.refresh_message();
191 return;
192 }
193 };
194
195 let line = format!(
196 " {label} line {ln}: `{orig}` → `{mut_}` ({elapsed:.1?})",
197 ln = mutant.line_number,
198 orig = truncate_str(&mutant.original, 40),
199 mut_ = truncate_str(&mutant.mutation.to_string(), 40),
200 elapsed = elapsed,
201 );
202 self.multi.suspend(|| {
203 let _ = foundry_common::sh_println!("{line}");
204 });
205 self.refresh_message();
206 }
207
208 pub fn clear(&mut self) {
210 for (_, am) in self.active_mutants.drain() {
211 am.pb.finish_and_clear();
212 }
213 self.overall_progress.finish_and_clear();
214 let _ = self.multi.clear();
215 }
216
217 pub fn finish(&mut self, message: &str) {
219 for (_, am) in self.active_mutants.drain() {
220 am.pb.finish_and_clear();
221 }
222 self.overall_progress.finish_with_message(message.to_string());
223 }
224
225 #[allow(dead_code)]
227 pub const fn num_workers(&self) -> usize {
228 self.num_workers
229 }
230}
231
232#[derive(Debug, Clone)]
234pub struct MutationProgress {
235 pub inner: Arc<Mutex<MutationProgressState>>,
236 pub cancelled: Arc<AtomicBool>,
237 pub completed: Arc<AtomicUsize>,
238 pub total: usize,
239}
240
241impl MutationProgress {
242 pub fn new(total_mutants: usize, num_workers: usize) -> Self {
243 Self::with_timeout(total_mutants, num_workers, None)
244 }
245
246 pub fn with_timeout(
247 total_mutants: usize,
248 num_workers: usize,
249 timeout_secs: Option<u32>,
250 ) -> Self {
251 Self {
252 inner: Arc::new(Mutex::new(MutationProgressState::with_timeout(
253 total_mutants,
254 num_workers,
255 timeout_secs,
256 ))),
257 cancelled: Arc::new(AtomicBool::new(false)),
258 completed: Arc::new(AtomicUsize::new(0)),
259 total: total_mutants,
260 }
261 }
262
263 pub fn is_cancelled(&self) -> bool {
265 self.cancelled.load(Ordering::SeqCst)
266 }
267
268 pub fn cancel(&self) {
270 self.cancelled.store(true, Ordering::SeqCst);
271 }
272
273 pub fn set_current_file(&self, file: &str) {
275 self.inner.lock().set_current_file(file);
276 }
277
278 pub fn start_mutant(&self, mutant: &Mutant) {
280 self.inner.lock().add_mutant_progress(mutant);
281 }
282
283 pub fn complete_mutant(&self, mutant: &Mutant, result: &MutationResult) -> usize {
285 let completed = self.completed.fetch_add(1, Ordering::SeqCst) + 1;
286 self.inner.lock().complete_mutant(mutant, result);
287 completed
288 }
289
290 pub fn clear(&self) {
292 MutationProgressState::clear(&mut self.inner.lock());
293 }
294
295 pub fn finish(&self, message: &str) {
297 self.inner.lock().finish(message);
298 }
299}
300
301fn truncate_str(s: &str, max_len: usize) -> String {
303 let s = s.trim();
304 if s.len() <= max_len {
305 return s.to_string();
306 }
307
308 let half = max_len.saturating_sub(3) / 2; let mid = s.len() / 2;
311 let start = mid.saturating_sub(half);
312 let end = (start + max_len.saturating_sub(3)).min(s.len());
313
314 if start == 0 {
315 format!("{}...", &s[..end])
316 } else if end == s.len() {
317 format!("...{}", &s[start..])
318 } else {
319 format!("...{}...", &s[start..end])
320 }
321}