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

lpenz / ogle / 15522185028

08 Jun 2025 08:18PM UTC coverage: 72.71% (+2.0%) from 70.713%
15522185028

push

github

lpenz
sys_input: improve coverage

4 of 4 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

381 of 524 relevant lines covered (72.71%)

1.82 hits per line

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

96.69
/src/sys_input.rs
1
// Copyright (C) 2025 Leandro Lisboa Penz <lpenz@lpenz.org>
2
// This file is subject to the terms and conditions defined in
3
// file 'LICENSE', which is part of this source code package.
4

5
//! Module that wraps system functions used as inputs
6
//!
7
//! Wrapping this makes it very easy to test the whole program.
8

9
use color_eyre::Result;
10
use pin_project::pin_project;
11
use std::cell::RefCell;
12
use std::collections::VecDeque;
13
use std::fmt;
14
use std::io;
15
use std::pin::Pin;
16
use std::process::ExitStatus;
17
use std::process::Stdio;
18
use std::task::{Context, Poll};
19
use tokio::process::Command;
20
use tokio_process_stream as tps;
21
use tokio_stream::Stream;
22
use tracing::instrument;
23

24
use crate::term_wrapper;
25
use crate::time_wrapper::Duration;
26
use crate::time_wrapper::Instant;
27

28
//////////////////////////////////////////////////////////////////////////////
29

30
/// A [`tokio::process::Command`] pseudo-wrapper that `impl Clone`.
31
#[derive(Debug, Default, Clone)]
32
pub struct Cmd(Vec<String>);
33

34
impl Cmd {
35
    pub fn get_command(self) -> Command {
6✔
36
        let mut command = Command::new(&self.0[0]);
6✔
37
        command.args(self.0.iter().skip(1));
6✔
38
        command.stdin(Stdio::null());
6✔
39
        command.stdout(Stdio::piped());
6✔
40
        command.stderr(Stdio::piped());
6✔
41
        command
6✔
42
    }
6✔
43
}
44

45
impl From<Vec<String>> for Cmd {
46
    fn from(s: Vec<String>) -> Cmd {
2✔
47
        Self(s)
2✔
48
    }
2✔
49
}
50

51
impl From<&[&str]> for Cmd {
52
    fn from(s: &[&str]) -> Cmd {
4✔
53
        Self(s.iter().map(|s| s.to_string()).collect::<Vec<_>>())
7✔
54
    }
4✔
55
}
56

57
impl fmt::Display for Cmd {
58
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1✔
59
        let joined = self.0.join(" ");
1✔
60
        write!(f, "{}", joined)
1✔
61
    }
1✔
62
}
63

64
//////////////////////////////////////////////////////////////////////////////
65

66
/// A clonable, PartialEq wrapper for [`tokio_process_stream::Item`]
67
#[derive(Debug, Clone, PartialEq, Eq)]
68
pub enum Item {
69
    /// A stdout line printed by the process.
70
    Stdout(String),
71
    /// A stderr line printed by the process.
72
    Stderr(String),
73
    /// The [`ExitStatus`](std::process::ExitStatus), yielded after the process exits.
74
    Done(Result<ExitStatus, io::ErrorKind>),
75
}
76

77
impl From<tps::Item<String>> for Item {
78
    fn from(item: tps::Item<String>) -> Self {
6✔
79
        match item {
6✔
80
            tps::Item::Stdout(s) => Item::Stdout(s),
1✔
81
            tps::Item::Stderr(s) => Item::Stderr(s),
1✔
82
            tps::Item::Done(result) => Item::Done(result.map_err(|e| e.kind())),
4✔
83
        }
84
    }
6✔
85
}
86

87
/// A mockable wrapper for [`tokio_process_stream::ProcessLineStream`].
88
#[pin_project(project = ProcessStreamProj)]
×
89
pub enum ProcessStream {
90
    /// Wrapper for [`tokio_process_stream::ProcessLineStream`].
91
    Real { stream: Box<tps::ProcessLineStream> },
92
    /// Mock for a running process stream that just returns items from
93
    /// a list. Useful for testing.
94
    Virtual { items: VecDeque<Item> },
95
}
96

97
impl std::fmt::Debug for ProcessStream {
98
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1✔
99
        match self {
1✔
100
            ProcessStream::Real { stream: _ } => f.debug_struct("ProcessStream::Real"),
×
101
            ProcessStream::Virtual { items: _ } => f.debug_struct("ProcessStream::Virtual"),
1✔
102
        }
103
        .finish()
1✔
104
    }
1✔
105
}
106

107
impl From<tps::ProcessLineStream> for ProcessStream {
108
    fn from(stream: tps::ProcessLineStream) -> Self {
4✔
109
        ProcessStream::Real {
4✔
110
            stream: Box::new(stream),
4✔
111
        }
4✔
112
    }
4✔
113
}
114

115
impl From<VecDeque<Item>> for ProcessStream {
116
    fn from(items: VecDeque<Item>) -> Self {
3✔
117
        ProcessStream::Virtual { items }
3✔
118
    }
3✔
119
}
120

121
impl Stream for ProcessStream {
122
    type Item = Item;
123

124
    #[instrument(level = "debug", ret, skip(cx))]
125
    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
126
        let this = self.project();
127
        match this {
128
            ProcessStreamProj::Real { stream } => {
129
                let next = Pin::new(stream).poll_next(cx);
130
                match next {
131
                    Poll::Ready(opt) => Poll::Ready(opt.map(|i| i.into())),
6✔
132
                    Poll::Pending => Poll::Pending,
133
                }
134
            }
135
            ProcessStreamProj::Virtual { items } => Poll::Ready(items.pop_front()),
136
        }
137
    }
138
}
139

140
//////////////////////////////////////////////////////////////////////////////
141

142
/// Wrap the system functions we use as inputs.
143
///
144
/// This wrapper makes testing easy.
145
pub trait SysInputApi: std::fmt::Debug + Clone + Default {
146
    fn now(&self) -> Instant;
147
    #[allow(dead_code)]
148
    fn size_checked(&self) -> Option<(u16, u16)>;
149
    fn run_command(&mut self, command: Cmd) -> Result<ProcessStream, std::io::Error>;
150
}
151

152
/// [`SysInputApi`] implementation of the real environment.
153
#[derive(Debug, Clone, Default)]
154
pub struct SysInputReal {}
155

156
impl SysInputApi for SysInputReal {
157
    fn now(&self) -> Instant {
4✔
158
        Instant::from(chrono::offset::Utc::now())
4✔
159
    }
4✔
160
    fn size_checked(&self) -> Option<(u16, u16)> {
×
161
        term_wrapper::size_checked()
×
UNCOV
162
    }
×
163
    fn run_command(&mut self, cmd: Cmd) -> Result<ProcessStream, std::io::Error> {
4✔
164
        let process_stream = tps::ProcessLineStream::try_from(cmd.get_command())?;
4✔
165
        Ok(ProcessStream::from(process_stream))
4✔
166
    }
4✔
167
}
168

169
/// [`SysInputApi`] implementation of a virtual environment, to be used in tests.
170
#[derive(Debug, Clone, Default)]
171
pub struct SysInputVirtual {
172
    now: RefCell<Instant>,
173
    items: VecDeque<Item>,
174
}
175

176
impl SysInputApi for SysInputVirtual {
177
    fn now(&self) -> Instant {
10✔
178
        let mut now_ref = self.now.borrow_mut();
10✔
179
        let now = *now_ref;
10✔
180
        *now_ref = &now + &Duration::seconds(1);
10✔
181
        now
10✔
182
    }
10✔
183
    fn size_checked(&self) -> Option<(u16, u16)> {
1✔
184
        Some((25, 80))
1✔
185
    }
1✔
186
    fn run_command(&mut self, _cmd: Cmd) -> Result<ProcessStream, std::io::Error> {
3✔
187
        let items = std::mem::take(&mut self.items);
3✔
188
        Ok(ProcessStream::from(items))
3✔
189
    }
3✔
190
}
191

192
#[cfg(test)]
193
impl SysInputVirtual {
194
    pub fn set_items(&mut self, items: Vec<Item>) {
3✔
195
        self.items = items.into_iter().collect();
3✔
196
    }
3✔
197
}
198

199
#[cfg(test)]
200
pub mod test {
201
    use color_eyre::eyre::eyre;
202
    use color_eyre::Result;
203
    use tokio_stream::StreamExt;
204

205
    use crate::sys_input::SysInputReal;
206
    use crate::sys_input::SysInputVirtual;
207

208
    use super::*;
209

210
    // Tests for SysInputReal with simple unix bins as we don't cover
211
    // it in downstream tests
212

213
    async fn stream_cmd(
4✔
214
        cmdstr: &[&str],
4✔
215
    ) -> Result<impl StreamExt<Item = Item> + std::marker::Unpin + Send + 'static> {
4✔
216
        let cmd = Cmd::from(cmdstr);
4✔
217
        let mut sys = SysInputReal::default();
4✔
218
        sys.run_command(cmd).map_err(|e| eyre!(e))
4✔
219
    }
4✔
220

221
    async fn stream_next<T>(stream: &mut T) -> Result<Item>
6✔
222
    where
6✔
223
        T: StreamExt<Item = Item> + std::marker::Unpin + Send + 'static,
6✔
224
    {
6✔
225
        stream.next().await.ok_or(eyre!("no item received"))
6✔
226
    }
6✔
227

228
    #[tokio::test]
229
    async fn test_true() -> Result<()> {
1✔
230
        let mut stream = stream_cmd(&["true"]).await?;
1✔
231
        let item = stream_next(&mut stream).await?;
1✔
232
        let Item::Done(sts) = item else {
1✔
233
            return Err(eyre!("unexpected stream item {:?}", item));
1✔
234
        };
1✔
235
        assert!(sts.unwrap().success());
1✔
236
        assert!(stream.next().await.is_none());
1✔
237
        Ok(())
1✔
238
    }
1✔
239

240
    #[tokio::test]
241
    async fn test_false() -> Result<()> {
1✔
242
        let mut stream = stream_cmd(&["false"]).await?;
1✔
243
        let item = stream_next(&mut stream).await?;
1✔
244
        let Item::Done(sts) = item else {
1✔
245
            return Err(eyre!("unexpected stream item {:?}", item));
1✔
246
        };
1✔
247
        assert!(!sts.unwrap().success());
1✔
248
        Ok(())
1✔
249
    }
1✔
250

251
    #[tokio::test]
252
    async fn test_echo() -> Result<()> {
1✔
253
        let mut stream = stream_cmd(&["echo", "test"]).await?;
1✔
254
        let item = stream_next(&mut stream).await?;
1✔
255
        let Item::Stdout(s) = item else {
1✔
256
            return Err(eyre!("unexpected stream item {:?}", item));
1✔
257
        };
1✔
258
        assert_eq!(s, "test");
1✔
259
        let item = stream_next(&mut stream).await?;
1✔
260
        let Item::Done(sts) = item else {
1✔
261
            return Err(eyre!("unexpected stream item {:?}", item));
1✔
262
        };
1✔
263
        assert!(sts.unwrap().success());
1✔
264
        Ok(())
1✔
265
    }
1✔
266

267
    #[tokio::test]
268
    async fn test_stderr() -> Result<()> {
1✔
269
        let mut stream = stream_cmd(&["/bin/sh", "-c", "echo test >&2"]).await?;
1✔
270
        let item = stream_next(&mut stream).await?;
1✔
271
        let Item::Stderr(s) = item else {
1✔
272
            return Err(eyre!("unexpected stream item {:?}", item));
1✔
273
        };
1✔
274
        assert_eq!(s, "test");
1✔
275
        let item = stream_next(&mut stream).await?;
1✔
276
        let Item::Done(sts) = item else {
1✔
277
            return Err(eyre!("unexpected stream item {:?}", item));
1✔
278
        };
1✔
279
        assert!(sts.unwrap().success());
1✔
280
        Ok(())
1✔
281
    }
1✔
282

283
    #[test]
284
    fn test_now() {
1✔
285
        let sys = SysInputReal::default();
1✔
286
        let now = sys.now();
1✔
287
        let now2 = sys.now();
1✔
288
        assert!(&now2 >= &now);
1✔
289
    }
1✔
290

291
    // A simple test for SysInputVirtual as we cover it better in
292
    // downstream tests
293

294
    #[tokio::test]
295
    async fn test_sysinputvirtual() -> Result<()> {
1✔
296
        let list = vec![
1✔
297
            Item::Stdout("stdout".into()),
1✔
298
            Item::Stderr("stderr".into()),
1✔
299
            Item::Done(Ok(ExitStatus::default())),
1✔
300
        ];
1✔
301
        let mut sys = SysInputVirtual::default();
1✔
302
        sys.set_items(list.clone());
1✔
303
        let cmd = Cmd::default();
1✔
304
        assert_eq!(format!("{}", cmd), "");
1✔
305
        let streamer = sys.run_command(cmd)?;
1✔
306
        assert_eq!(format!("{:?}", streamer), "ProcessStream::Virtual");
1✔
307
        let streamed = streamer.collect::<Vec<_>>().await;
1✔
308
        assert_eq!(streamed, list);
1✔
309
        assert_eq!(sys.now(), Instant::default());
1✔
310
        assert_eq!(sys.now(), &Instant::default() + &Duration::seconds(1));
1✔
311
        assert_eq!(sys.size_checked(), Some((25, 80)));
1✔
312
        Ok(())
1✔
313
    }
1✔
314
}
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

© 2026 Coveralls, Inc