• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rraval / git-nomad / 7074366391

03 Dec 2023 03:17AM UTC coverage: 98.531%. First build
7074366391

Pull #141

github

rraval
Explicit output control with Renderer

Gets rid of ad-hoc `println!` and `eprintln!` in favour of plumbing an
explicit `Renderer` that wraps a `Term`. This fixes some glitchy output
where progress bars were interfering with normal text (most noticeable
in `purge` since it's all progress bars).
Pull Request #141: Explicit output control with Renderer

374 of 406 new or added lines in 6 files covered. (92.12%)

2481 of 2518 relevant lines covered (98.53%)

22.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.46
/src/renderer.rs
1
use anyhow::Result;
2
use console::Term;
3
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
4
use std::{borrow::Cow, io::Write, time::Duration};
5

6
pub trait Renderer {
7
    fn writer<T>(&mut self, func: impl FnOnce(&mut dyn Write) -> Result<T>) -> Result<T>;
8

9
    fn are_spinners_visible(&self) -> bool;
10

11
    fn spinner<T>(
12
        &mut self,
13
        description: impl Into<Cow<'static, str>>,
14
        func: impl FnOnce() -> Result<T>,
15
    ) -> Result<T>;
16
}
17

18
pub struct TerminalRenderer(Term);
19

20
impl TerminalRenderer {
21
    pub fn stdout() -> Self {
3✔
22
        Self(Term::buffered_stdout())
3✔
23
    }
3✔
24
}
25

26
impl Renderer for TerminalRenderer {
27
    fn writer<T>(&mut self, func: impl FnOnce(&mut dyn Write) -> Result<T>) -> Result<T> {
1✔
28
        let ret = func(&mut self.0)?;
1✔
29
        self.0.flush()?;
1✔
30
        Ok(ret)
1✔
31
    }
1✔
32

33
    fn are_spinners_visible(&self) -> bool {
2✔
34
        self.0.is_term()
2✔
35
    }
2✔
36

37
    fn spinner<T>(
1✔
38
        &mut self,
1✔
39
        description: impl Into<Cow<'static, str>>,
1✔
40
        func: impl FnOnce() -> Result<T>,
1✔
41
    ) -> Result<T> {
1✔
42
        // indicatif does this implicitly but check this explicitly to avoid adding newlines when
1✔
43
        // the spinner won't be shown. As a bonus, it also avoids the overhead of allocating the
1✔
44
        // progress bar machinery.
1✔
45
        if !self.are_spinners_visible() {
1✔
46
            return func();
1✔
NEW
47
        }
×
NEW
48

×
NEW
49
        let spinner =
×
NEW
50
            ProgressBar::with_draw_target(None, ProgressDrawTarget::term(self.0.clone(), 10));
×
NEW
51
        spinner.set_style(
×
NEW
52
            ProgressStyle::default_spinner()
×
NEW
53
                .tick_strings(&[" ..", ". .", ".. ", "..."])
×
NEW
54
                .template("{msg}{spinner} {elapsed}")
×
NEW
55
                .unwrap(),
×
NEW
56
        );
×
NEW
57
        spinner.set_message(description);
×
NEW
58
        spinner.enable_steady_tick(Duration::from_millis(150));
×
NEW
59

×
NEW
60
        let ret = func();
×
NEW
61
        spinner.finish();
×
NEW
62

×
NEW
63
        // The finish call merely redraws the progress bar in its final state. The line needs to be
×
NEW
64
        // explicitly terminated.
×
NEW
65
        writeln!(self.0)?;
×
NEW
66
        self.0.flush()?;
×
67

NEW
68
        ret
×
69
    }
1✔
70
}
71

72
/// Adds a newline to separate output from spinners, but that's only necessary if spinners are even
73
/// being displayed.
74
pub fn add_newline_if_spinners_are_visible(renderer: &mut impl Renderer) -> Result<()> {
3✔
75
    if renderer.are_spinners_visible() {
3✔
76
        renderer.writer(|w| {
1✔
77
            writeln!(w)?;
1✔
78
            Ok(())
1✔
79
        })?;
1✔
80
    }
2✔
81

82
    Ok(())
3✔
83
}
3✔
84

85
#[cfg(test)]
86
pub mod test_terminal {
87
    use anyhow::Context;
88

89
    use crate::renderer::{TerminalRenderer, Renderer};
90

91
    #[test]
1✔
92
    fn writer() {
1✔
93
        let mut renderer = TerminalRenderer::stdout();
1✔
94
        renderer
1✔
95
            .writer(|w| write!(w, "").context("write in test"))
1✔
96
            .unwrap();
1✔
97
    }
1✔
98

99
    #[test]
1✔
100
    fn are_spinners_visible() {
1✔
101
        TerminalRenderer::stdout().are_spinners_visible();
1✔
102
    }
1✔
103

104
    #[test]
1✔
105
    fn spinner() {
1✔
106
        let mut renderer = TerminalRenderer::stdout();
1✔
107
        let mut func_called = false;
1✔
108
        renderer.spinner("Spinning", || {
1✔
109
            func_called = true;
1✔
110
            Ok(())
1✔
111
        }).unwrap();
1✔
112
        assert!(func_called);
1✔
113
    }
1✔
114
}
115

116
#[cfg(test)]
117
pub mod test {
118
    use std::io::Write;
119
    use std::{borrow::Cow, io};
120

121
    use anyhow::{Context, Result};
122

123
    use super::{Renderer, add_newline_if_spinners_are_visible};
124

125
    pub struct MemoryRenderer(Vec<u8>);
126

127
    impl MemoryRenderer {
128
        pub fn new() -> Self {
8✔
129
            Self(Vec::new())
8✔
130
        }
8✔
131

132
        pub fn as_str(&self) -> &str {
8✔
133
            std::str::from_utf8(self.0.as_slice()).expect("tests should have utf8 output")
8✔
134
        }
8✔
135
    }
136

137
    impl Renderer for MemoryRenderer {
138
        fn writer<T>(&mut self, func: impl FnOnce(&mut dyn Write) -> Result<T>) -> Result<T> {
6✔
139
            func(&mut self.0)
6✔
140
        }
6✔
141

142
        fn are_spinners_visible(&self) -> bool {
1✔
143
            true
1✔
144
        }
1✔
145

146
        fn spinner<T>(
147
            &mut self,
148
            description: impl Into<Cow<'static, str>>,
149
            func: impl FnOnce() -> Result<T>,
150
        ) -> Result<T> {
151
            writeln!(self.0, "{}...", description.into())?;
1✔
152
            func()
1✔
153
        }
1✔
154
    }
155

156
    pub struct NoRenderer;
157

158
    impl Renderer for NoRenderer {
159
        fn writer<T>(&mut self, func: impl FnOnce(&mut dyn Write) -> Result<T>) -> Result<T> {
367✔
160
            func(&mut io::sink())
367✔
161
        }
367✔
162

163
        fn are_spinners_visible(&self) -> bool {
2✔
164
            false
2✔
165
        }
2✔
166

167
        fn spinner<T>(
1✔
168
            &mut self,
1✔
169
            _description: impl Into<Cow<'static, str>>,
1✔
170
            func: impl FnOnce() -> Result<T>,
1✔
171
        ) -> Result<T> {
1✔
172
            func()
1✔
173
        }
1✔
174
    }
175

176
    #[test]
1✔
177
    fn writer() {
1✔
178
        let mut renderer = MemoryRenderer::new();
1✔
179
        renderer
1✔
180
            .writer(|w| writeln!(w, "foo").context("write in test"))
1✔
181
            .unwrap();
1✔
182
        assert_eq!(renderer.as_str(), "foo\n");
1✔
183
    }
1✔
184

185
    #[test]
1✔
186
    fn spinner() {
1✔
187
        let mut renderer = MemoryRenderer::new();
1✔
188
        let mut func_called = false;
1✔
189
        renderer.spinner("Spinning", || {
1✔
190
            func_called = true;
1✔
191
            Ok(())
1✔
192
        }).unwrap();
1✔
193

1✔
194
        assert_eq!(renderer.as_str(), "Spinning...\n");
1✔
195
        assert!(func_called);
1✔
196
    }
1✔
197

198
    #[test]
1✔
199
    fn add_newline() {
1✔
200
        let mut renderer = MemoryRenderer::new();
1✔
201
        add_newline_if_spinners_are_visible(&mut renderer).unwrap();
1✔
202
        assert_eq!(renderer.as_str(), "\n");
1✔
203
    }
1✔
204
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc