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