indicatif/
multi.rs

1use std::fmt::{Debug, Formatter};
2use std::io;
3use std::sync::{Arc, RwLock};
4use std::thread::panicking;
5#[cfg(not(target_arch = "wasm32"))]
6use std::time::Instant;
7
8use crate::draw_target::{
9    visual_line_count, DrawState, DrawStateWrapper, LineAdjust, ProgressDrawTarget, VisualLines,
10};
11use crate::progress_bar::ProgressBar;
12#[cfg(target_arch = "wasm32")]
13use web_time::Instant;
14
15/// Manages multiple progress bars from different threads
16#[derive(Debug, Clone)]
17pub struct MultiProgress {
18    pub(crate) state: Arc<RwLock<MultiState>>,
19}
20
21impl Default for MultiProgress {
22    fn default() -> Self {
23        Self::with_draw_target(ProgressDrawTarget::stderr())
24    }
25}
26
27impl MultiProgress {
28    /// Creates a new multi progress object.
29    ///
30    /// Progress bars added to this object by default draw directly to stderr, and refresh
31    /// a maximum of 15 times a second. To change the refresh rate [set] the [draw target] to
32    /// one with a different refresh rate.
33    ///
34    /// [set]: MultiProgress::set_draw_target
35    /// [draw target]: ProgressDrawTarget
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Creates a new multi progress object with the given draw target.
41    pub fn with_draw_target(draw_target: ProgressDrawTarget) -> Self {
42        Self {
43            state: Arc::new(RwLock::new(MultiState::new(draw_target))),
44        }
45    }
46
47    /// Sets a different draw target for the multiprogress bar.
48    ///
49    /// Use [`MultiProgress::with_draw_target`] to set the draw target during creation.
50    pub fn set_draw_target(&self, target: ProgressDrawTarget) {
51        let mut state = self.state.write().unwrap();
52        state.draw_target.disconnect(Instant::now());
53        state.draw_target = target;
54    }
55
56    /// Set whether we should try to move the cursor when possible instead of clearing lines.
57    ///
58    /// This can reduce flickering, but do not enable it if you intend to change the number of
59    /// progress bars.
60    pub fn set_move_cursor(&self, move_cursor: bool) {
61        self.state
62            .write()
63            .unwrap()
64            .draw_target
65            .set_move_cursor(move_cursor);
66    }
67
68    /// Set alignment flag
69    pub fn set_alignment(&self, alignment: MultiProgressAlignment) {
70        self.state.write().unwrap().alignment = alignment;
71    }
72
73    /// Adds a progress bar.
74    ///
75    /// The progress bar added will have the draw target changed to a
76    /// remote draw target that is intercepted by the multi progress
77    /// object overriding custom [`ProgressDrawTarget`] settings.
78    ///
79    /// The progress bar will be positioned below all other bars currently
80    /// in the [`MultiProgress`].
81    ///
82    /// Adding a progress bar that is already a member of the [`MultiProgress`]
83    /// will have no effect.
84    pub fn add(&self, pb: ProgressBar) -> ProgressBar {
85        self.internalize(InsertLocation::End, pb)
86    }
87
88    /// Inserts a progress bar.
89    ///
90    /// The progress bar inserted at position `index` will have the draw
91    /// target changed to a remote draw target that is intercepted by the
92    /// multi progress object overriding custom [`ProgressDrawTarget`] settings.
93    ///
94    /// If `index >= MultiProgressState::objects.len()`, the progress bar
95    /// is added to the end of the list.
96    ///
97    /// Inserting a progress bar that is already a member of the [`MultiProgress`]
98    /// will have no effect.
99    pub fn insert(&self, index: usize, pb: ProgressBar) -> ProgressBar {
100        self.internalize(InsertLocation::Index(index), pb)
101    }
102
103    /// Inserts a progress bar from the back.
104    ///
105    /// The progress bar inserted at position `MultiProgressState::objects.len() - index`
106    /// will have the draw target changed to a remote draw target that is
107    /// intercepted by the multi progress object overriding custom
108    /// [`ProgressDrawTarget`] settings.
109    ///
110    /// If `index >= MultiProgressState::objects.len()`, the progress bar
111    /// is added to the start of the list.
112    ///
113    /// Inserting a progress bar that is already a member of the [`MultiProgress`]
114    /// will have no effect.
115    pub fn insert_from_back(&self, index: usize, pb: ProgressBar) -> ProgressBar {
116        self.internalize(InsertLocation::IndexFromBack(index), pb)
117    }
118
119    /// Inserts a progress bar before an existing one.
120    ///
121    /// The progress bar added will have the draw target changed to a
122    /// remote draw target that is intercepted by the multi progress
123    /// object overriding custom [`ProgressDrawTarget`] settings.
124    ///
125    /// Inserting a progress bar that is already a member of the [`MultiProgress`]
126    /// will have no effect.
127    pub fn insert_before(&self, before: &ProgressBar, pb: ProgressBar) -> ProgressBar {
128        self.internalize(InsertLocation::Before(before.index().unwrap()), pb)
129    }
130
131    /// Inserts a progress bar after an existing one.
132    ///
133    /// The progress bar added will have the draw target changed to a
134    /// remote draw target that is intercepted by the multi progress
135    /// object overriding custom [`ProgressDrawTarget`] settings.
136    ///
137    /// Inserting a progress bar that is already a member of the [`MultiProgress`]
138    /// will have no effect.
139    pub fn insert_after(&self, after: &ProgressBar, pb: ProgressBar) -> ProgressBar {
140        self.internalize(InsertLocation::After(after.index().unwrap()), pb)
141    }
142
143    /// Removes a progress bar.
144    ///
145    /// The progress bar is removed only if it was previously inserted or added
146    /// by the methods [`MultiProgress::insert`] or [`MultiProgress::add`].
147    /// If the passed progress bar does not satisfy the condition above,
148    /// the `remove` method does nothing.
149    pub fn remove(&self, pb: &ProgressBar) {
150        let mut state = pb.state();
151        let idx = match &state.draw_target.remote() {
152            Some((state, idx)) => {
153                // Check that this progress bar is owned by the current MultiProgress.
154                assert!(Arc::ptr_eq(&self.state, state));
155                *idx
156            }
157            _ => return,
158        };
159
160        state.draw_target = ProgressDrawTarget::hidden();
161        self.state.write().unwrap().remove_idx(idx);
162    }
163
164    fn internalize(&self, location: InsertLocation, pb: ProgressBar) -> ProgressBar {
165        let mut state = self.state.write().unwrap();
166        let idx = state.insert(location);
167        drop(state);
168
169        pb.set_draw_target(ProgressDrawTarget::new_remote(self.state.clone(), idx));
170        pb
171    }
172
173    /// Print a log line above all progress bars in the [`MultiProgress`]
174    ///
175    /// If the draw target is hidden (e.g. when standard output is not a terminal), `println()`
176    /// will not do anything.
177    pub fn println<I: AsRef<str>>(&self, msg: I) -> io::Result<()> {
178        let mut state = self.state.write().unwrap();
179        state.println(msg, Instant::now())
180    }
181
182    /// Hide all progress bars temporarily, execute `f`, then redraw the [`MultiProgress`]
183    ///
184    /// Executes 'f' even if the draw target is hidden.
185    ///
186    /// Useful for external code that writes to the standard output.
187    ///
188    /// **Note:** The internal lock is held while `f` is executed. Other threads trying to print
189    /// anything on the progress bar will be blocked until `f` finishes.
190    /// Therefore, it is recommended to avoid long-running operations in `f`.
191    pub fn suspend<F: FnOnce() -> R, R>(&self, f: F) -> R {
192        let mut state = self.state.write().unwrap();
193        state.suspend(f, Instant::now())
194    }
195
196    pub fn clear(&self) -> io::Result<()> {
197        self.state.write().unwrap().clear(Instant::now())
198    }
199
200    pub fn is_hidden(&self) -> bool {
201        self.state.read().unwrap().is_hidden()
202    }
203}
204
205#[derive(Debug)]
206pub(crate) struct MultiState {
207    /// The collection of states corresponding to progress bars
208    members: Vec<MultiStateMember>,
209    /// Set of removed bars, should have corresponding members in the `members` vector with a
210    /// `draw_state` of `None`.
211    free_set: Vec<usize>,
212    /// Indices to the `draw_states` to maintain correct visual order
213    ordering: Vec<usize>,
214    /// Target for draw operation for MultiProgress
215    draw_target: ProgressDrawTarget,
216    /// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top`
217    alignment: MultiProgressAlignment,
218    /// Lines to be drawn above everything else in the MultiProgress. These specifically come from
219    /// calling `ProgressBar::println` on a pb that is connected to a `MultiProgress`.
220    orphan_lines: Vec<String>,
221    /// The count of currently visible zombie lines.
222    zombie_lines_count: VisualLines,
223}
224
225impl MultiState {
226    fn new(draw_target: ProgressDrawTarget) -> Self {
227        Self {
228            members: vec![],
229            free_set: vec![],
230            ordering: vec![],
231            draw_target,
232            alignment: MultiProgressAlignment::default(),
233            orphan_lines: Vec::new(),
234            zombie_lines_count: VisualLines::default(),
235        }
236    }
237
238    pub(crate) fn mark_zombie(&mut self, index: usize) {
239        let width = self.width().map(usize::from);
240
241        let member = &mut self.members[index];
242
243        // If the zombie is the first visual bar then we can reap it right now instead of
244        // deferring it to the next draw.
245        if index != self.ordering.first().copied().unwrap() {
246            member.is_zombie = true;
247            return;
248        }
249
250        let line_count = member
251            .draw_state
252            .as_ref()
253            .zip(width)
254            .map(|(d, width)| d.visual_line_count(.., width))
255            .unwrap_or_default();
256
257        // Track the total number of zombie lines on the screen
258        self.zombie_lines_count = self.zombie_lines_count.saturating_add(line_count);
259
260        // Make `DrawTarget` forget about the zombie lines so that they aren't cleared on next draw.
261        self.draw_target
262            .adjust_last_line_count(LineAdjust::Keep(line_count));
263
264        self.remove_idx(index);
265    }
266
267    pub(crate) fn draw(
268        &mut self,
269        mut force_draw: bool,
270        extra_lines: Option<Vec<String>>,
271        now: Instant,
272    ) -> io::Result<()> {
273        if panicking() {
274            return Ok(());
275        }
276
277        let width = match self.width() {
278            Some(width) => width as usize,
279            None => return Ok(()),
280        };
281
282        // Assumption: if extra_lines is not None, then it has at least one line
283        debug_assert_eq!(
284            extra_lines.is_some(),
285            extra_lines.as_ref().map(Vec::len).unwrap_or_default() > 0
286        );
287
288        let mut reap_indices = vec![];
289
290        // Reap all consecutive 'zombie' progress bars from head of the list.
291        let mut adjust = VisualLines::default();
292        for &index in &self.ordering {
293            let member = &self.members[index];
294            if !member.is_zombie {
295                break;
296            }
297
298            let line_count = member
299                .draw_state
300                .as_ref()
301                .map(|d| d.visual_line_count(.., width))
302                .unwrap_or_default();
303            // Track the total number of zombie lines on the screen.
304            self.zombie_lines_count += line_count;
305
306            // Track the number of zombie lines that will be drawn by this call to draw.
307            adjust += line_count;
308
309            reap_indices.push(index);
310        }
311
312        // If this draw is due to a `println`, then we need to erase all the zombie lines.
313        // This is because `println` is supposed to appear above all other elements in the
314        // `MultiProgress`.
315        if extra_lines.is_some() {
316            self.draw_target
317                .adjust_last_line_count(LineAdjust::Clear(self.zombie_lines_count));
318            self.zombie_lines_count = VisualLines::default();
319        }
320
321        let orphan_visual_line_count = visual_line_count(&self.orphan_lines, width);
322        force_draw |= orphan_visual_line_count > VisualLines::default();
323        let mut drawable = match self.draw_target.drawable(force_draw, now) {
324            Some(drawable) => drawable,
325            None => return Ok(()),
326        };
327
328        let mut draw_state = drawable.state();
329        draw_state.orphan_lines_count = self.orphan_lines.len();
330        draw_state.alignment = self.alignment;
331
332        if let Some(extra_lines) = &extra_lines {
333            draw_state.lines.extend_from_slice(extra_lines.as_slice());
334            draw_state.orphan_lines_count += extra_lines.len();
335        }
336
337        // Add lines from `ProgressBar::println` call.
338        draw_state.lines.append(&mut self.orphan_lines);
339
340        for index in &self.ordering {
341            let member = &self.members[*index];
342            if let Some(state) = &member.draw_state {
343                draw_state.lines.extend_from_slice(&state.lines[..]);
344            }
345        }
346
347        drop(draw_state);
348        let drawable = drawable.draw();
349
350        for index in reap_indices {
351            self.remove_idx(index);
352        }
353
354        // The zombie lines were drawn for the last time, so make `DrawTarget` forget about them
355        // so they aren't cleared on next draw.
356        if extra_lines.is_none() {
357            self.draw_target
358                .adjust_last_line_count(LineAdjust::Keep(adjust));
359        }
360
361        drawable
362    }
363
364    pub(crate) fn println<I: AsRef<str>>(&mut self, msg: I, now: Instant) -> io::Result<()> {
365        let msg = msg.as_ref();
366
367        // If msg is "", make sure a line is still printed
368        let lines: Vec<String> = match msg.is_empty() {
369            false => msg.lines().map(Into::into).collect(),
370            true => vec![String::new()],
371        };
372
373        self.draw(true, Some(lines), now)
374    }
375
376    pub(crate) fn draw_state(&mut self, idx: usize) -> DrawStateWrapper<'_> {
377        let member = self.members.get_mut(idx).unwrap();
378        // alignment is handled by the `MultiProgress`'s underlying draw target, so there is no
379        // point in propagating it here.
380        let state = member.draw_state.get_or_insert(DrawState::default());
381
382        DrawStateWrapper::for_multi(state, &mut self.orphan_lines)
383    }
384
385    pub(crate) fn is_hidden(&self) -> bool {
386        self.draw_target.is_hidden()
387    }
388
389    pub(crate) fn suspend<F: FnOnce() -> R, R>(&mut self, f: F, now: Instant) -> R {
390        self.clear(now).unwrap();
391        let ret = f();
392        self.draw(true, None, Instant::now()).unwrap();
393        ret
394    }
395
396    pub(crate) fn width(&self) -> Option<u16> {
397        self.draw_target.width()
398    }
399
400    fn insert(&mut self, location: InsertLocation) -> usize {
401        let idx = if let Some(idx) = self.free_set.pop() {
402            self.members[idx] = MultiStateMember::default();
403            idx
404        } else {
405            self.members.push(MultiStateMember::default());
406            self.members.len() - 1
407        };
408
409        match location {
410            InsertLocation::End => self.ordering.push(idx),
411            InsertLocation::Index(pos) => {
412                let pos = Ord::min(pos, self.ordering.len());
413                self.ordering.insert(pos, idx);
414            }
415            InsertLocation::IndexFromBack(pos) => {
416                let pos = self.ordering.len().saturating_sub(pos);
417                self.ordering.insert(pos, idx);
418            }
419            InsertLocation::After(after_idx) => {
420                let pos = self.ordering.iter().position(|i| *i == after_idx).unwrap();
421                self.ordering.insert(pos + 1, idx);
422            }
423            InsertLocation::Before(before_idx) => {
424                let pos = self.ordering.iter().position(|i| *i == before_idx).unwrap();
425                self.ordering.insert(pos, idx);
426            }
427        }
428
429        assert_eq!(
430            self.len(),
431            self.ordering.len(),
432            "Draw state is inconsistent"
433        );
434
435        idx
436    }
437
438    fn clear(&mut self, now: Instant) -> io::Result<()> {
439        match self.draw_target.drawable(true, now) {
440            Some(mut drawable) => {
441                // Make the clear operation also wipe out zombie lines
442                drawable.adjust_last_line_count(LineAdjust::Clear(self.zombie_lines_count));
443                self.zombie_lines_count = VisualLines::default();
444                drawable.clear()
445            }
446            None => Ok(()),
447        }
448    }
449
450    fn remove_idx(&mut self, idx: usize) {
451        if self.free_set.contains(&idx) {
452            return;
453        }
454
455        self.members[idx] = MultiStateMember::default();
456        self.free_set.push(idx);
457        self.ordering.retain(|&x| x != idx);
458
459        assert_eq!(
460            self.len(),
461            self.ordering.len(),
462            "Draw state is inconsistent"
463        );
464    }
465
466    fn len(&self) -> usize {
467        self.members.len() - self.free_set.len()
468    }
469}
470
471#[derive(Default)]
472struct MultiStateMember {
473    /// Draw state will be `None` for members that haven't been drawn before, or for entries that
474    /// correspond to something in the free set.
475    draw_state: Option<DrawState>,
476    /// Whether the corresponding progress bar (more precisely, `BarState`) has been dropped.
477    is_zombie: bool,
478}
479
480impl Debug for MultiStateMember {
481    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
482        f.debug_struct("MultiStateElement")
483            .field("draw_state", &self.draw_state)
484            .field("is_zombie", &self.is_zombie)
485            .finish_non_exhaustive()
486    }
487}
488
489/// Vertical alignment of a multi progress.
490///
491/// The alignment controls how the multi progress is aligned if some of its progress bars get removed.
492/// E.g. [`Top`](MultiProgressAlignment::Top) alignment (default), when _progress bar 2_ is removed:
493/// ```ignore
494/// [0/100] progress bar 1        [0/100] progress bar 1
495/// [0/100] progress bar 2   =>   [0/100] progress bar 3
496/// [0/100] progress bar 3
497/// ```
498///
499/// [`Bottom`](MultiProgressAlignment::Bottom) alignment
500/// ```ignore
501/// [0/100] progress bar 1
502/// [0/100] progress bar 2   =>   [0/100] progress bar 1
503/// [0/100] progress bar 3        [0/100] progress bar 3
504/// ```
505#[derive(Debug, Copy, Clone)]
506pub enum MultiProgressAlignment {
507    Top,
508    Bottom,
509}
510
511impl Default for MultiProgressAlignment {
512    fn default() -> Self {
513        Self::Top
514    }
515}
516
517enum InsertLocation {
518    End,
519    Index(usize),
520    IndexFromBack(usize),
521    After(usize),
522    Before(usize),
523}
524
525#[cfg(test)]
526mod tests {
527    use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
528
529    #[test]
530    fn late_pb_drop() {
531        let pb = ProgressBar::new(10);
532        let mpb = MultiProgress::new();
533        // This clone call is required to trigger a now fixed bug.
534        // See <https://github.com/console-rs/indicatif/pull/141> for context
535        #[allow(clippy::redundant_clone)]
536        mpb.add(pb.clone());
537    }
538
539    #[test]
540    fn progress_bar_sync_send() {
541        let _: Box<dyn Sync> = Box::new(ProgressBar::new(1));
542        let _: Box<dyn Send> = Box::new(ProgressBar::new(1));
543        let _: Box<dyn Sync> = Box::new(MultiProgress::new());
544        let _: Box<dyn Send> = Box::new(MultiProgress::new());
545    }
546
547    #[test]
548    fn multi_progress_hidden() {
549        let mpb = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
550        let pb = mpb.add(ProgressBar::new(123));
551        pb.finish();
552    }
553
554    #[test]
555    fn multi_progress_modifications() {
556        let mp = MultiProgress::new();
557        let p0 = mp.add(ProgressBar::new(1));
558        let p1 = mp.add(ProgressBar::new(1));
559        let p2 = mp.add(ProgressBar::new(1));
560        let p3 = mp.add(ProgressBar::new(1));
561        mp.remove(&p2);
562        mp.remove(&p1);
563        let p4 = mp.insert(1, ProgressBar::new(1));
564
565        let state = mp.state.read().unwrap();
566        // the removed place for p1 is reused
567        assert_eq!(state.members.len(), 4);
568        assert_eq!(state.len(), 3);
569
570        // free_set may contain 1 or 2
571        match state.free_set.last() {
572            Some(1) => {
573                assert_eq!(state.ordering, vec![0, 2, 3]);
574                assert!(state.members[1].draw_state.is_none());
575                assert_eq!(p4.index().unwrap(), 2);
576            }
577            Some(2) => {
578                assert_eq!(state.ordering, vec![0, 1, 3]);
579                assert!(state.members[2].draw_state.is_none());
580                assert_eq!(p4.index().unwrap(), 1);
581            }
582            _ => unreachable!(),
583        }
584
585        assert_eq!(p0.index().unwrap(), 0);
586        assert_eq!(p1.index(), None);
587        assert_eq!(p2.index(), None);
588        assert_eq!(p3.index().unwrap(), 3);
589    }
590
591    #[test]
592    fn multi_progress_insert_from_back() {
593        let mp = MultiProgress::new();
594        let p0 = mp.add(ProgressBar::new(1));
595        let p1 = mp.add(ProgressBar::new(1));
596        let p2 = mp.add(ProgressBar::new(1));
597        let p3 = mp.insert_from_back(1, ProgressBar::new(1));
598        let p4 = mp.insert_from_back(10, ProgressBar::new(1));
599
600        let state = mp.state.read().unwrap();
601        assert_eq!(state.ordering, vec![4, 0, 1, 3, 2]);
602        assert_eq!(p0.index().unwrap(), 0);
603        assert_eq!(p1.index().unwrap(), 1);
604        assert_eq!(p2.index().unwrap(), 2);
605        assert_eq!(p3.index().unwrap(), 3);
606        assert_eq!(p4.index().unwrap(), 4);
607    }
608
609    #[test]
610    fn multi_progress_insert_after() {
611        let mp = MultiProgress::new();
612        let p0 = mp.add(ProgressBar::new(1));
613        let p1 = mp.add(ProgressBar::new(1));
614        let p2 = mp.add(ProgressBar::new(1));
615        let p3 = mp.insert_after(&p2, ProgressBar::new(1));
616        let p4 = mp.insert_after(&p0, ProgressBar::new(1));
617
618        let state = mp.state.read().unwrap();
619        assert_eq!(state.ordering, vec![0, 4, 1, 2, 3]);
620        assert_eq!(p0.index().unwrap(), 0);
621        assert_eq!(p1.index().unwrap(), 1);
622        assert_eq!(p2.index().unwrap(), 2);
623        assert_eq!(p3.index().unwrap(), 3);
624        assert_eq!(p4.index().unwrap(), 4);
625    }
626
627    #[test]
628    fn multi_progress_insert_before() {
629        let mp = MultiProgress::new();
630        let p0 = mp.add(ProgressBar::new(1));
631        let p1 = mp.add(ProgressBar::new(1));
632        let p2 = mp.add(ProgressBar::new(1));
633        let p3 = mp.insert_before(&p0, ProgressBar::new(1));
634        let p4 = mp.insert_before(&p2, ProgressBar::new(1));
635
636        let state = mp.state.read().unwrap();
637        assert_eq!(state.ordering, vec![3, 0, 1, 4, 2]);
638        assert_eq!(p0.index().unwrap(), 0);
639        assert_eq!(p1.index().unwrap(), 1);
640        assert_eq!(p2.index().unwrap(), 2);
641        assert_eq!(p3.index().unwrap(), 3);
642        assert_eq!(p4.index().unwrap(), 4);
643    }
644
645    #[test]
646    fn multi_progress_insert_before_and_after() {
647        let mp = MultiProgress::new();
648        let p0 = mp.add(ProgressBar::new(1));
649        let p1 = mp.add(ProgressBar::new(1));
650        let p2 = mp.add(ProgressBar::new(1));
651        let p3 = mp.insert_before(&p0, ProgressBar::new(1));
652        let p4 = mp.insert_after(&p3, ProgressBar::new(1));
653        let p5 = mp.insert_after(&p3, ProgressBar::new(1));
654        let p6 = mp.insert_before(&p1, ProgressBar::new(1));
655
656        let state = mp.state.read().unwrap();
657        assert_eq!(state.ordering, vec![3, 5, 4, 0, 6, 1, 2]);
658        assert_eq!(p0.index().unwrap(), 0);
659        assert_eq!(p1.index().unwrap(), 1);
660        assert_eq!(p2.index().unwrap(), 2);
661        assert_eq!(p3.index().unwrap(), 3);
662        assert_eq!(p4.index().unwrap(), 4);
663        assert_eq!(p5.index().unwrap(), 5);
664        assert_eq!(p6.index().unwrap(), 6);
665    }
666
667    #[test]
668    fn multi_progress_multiple_remove() {
669        let mp = MultiProgress::new();
670        let p0 = mp.add(ProgressBar::new(1));
671        let p1 = mp.add(ProgressBar::new(1));
672        // double remove beyond the first one have no effect
673        mp.remove(&p0);
674        mp.remove(&p0);
675        mp.remove(&p0);
676
677        let state = mp.state.read().unwrap();
678        // the removed place for p1 is reused
679        assert_eq!(state.members.len(), 2);
680        assert_eq!(state.free_set.len(), 1);
681        assert_eq!(state.len(), 1);
682        assert!(state.members[0].draw_state.is_none());
683        assert_eq!(state.free_set.last(), Some(&0));
684
685        assert_eq!(state.ordering, vec![1]);
686        assert_eq!(p0.index(), None);
687        assert_eq!(p1.index().unwrap(), 1);
688    }
689
690    #[test]
691    fn mp_no_crash_double_add() {
692        let mp = MultiProgress::new();
693        let pb = mp.add(ProgressBar::new(10));
694        mp.add(pb);
695    }
696}