cargo_tarpaulin/statemachine/
mod.rs

1use crate::config::{Config, TraceEngine};
2use crate::errors::RunError;
3use crate::event_log::*;
4use crate::traces::*;
5use crate::LineAnalysis;
6use crate::TestHandle;
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::time::Instant;
10use tracing::error;
11
12pub mod instrumented;
13cfg_if::cfg_if! {
14    if #[cfg(ptrace_supported)] {
15        pub mod linux;
16        pub use linux::ProcessInfo;
17    }
18}
19pub fn create_state_machine<'a>(
20    test: impl Into<TestHandle>,
21    traces: &'a mut TraceMap,
22    source_analysis: &'a HashMap<PathBuf, LineAnalysis>,
23    config: &'a Config,
24    event_log: &'a Option<EventLog>,
25) -> (TestState, Box<dyn StateData + 'a>) {
26    match config.engine() {
27        TraceEngine::Ptrace => {
28            cfg_if::cfg_if! {
29                if #[cfg(ptrace_supported)] {
30                    let (state, machine) = linux::create_state_machine(test, traces, source_analysis, config, event_log);
31                    (state, Box::new(machine))
32                } else {
33                    error!("The ptrace backend is not supported on this system");
34                    panic!()
35                }
36            }
37        }
38        // Should never be auto so ignore our normal rules
39        TraceEngine::Llvm | TraceEngine::Auto => {
40            let (state, machine) = instrumented::create_state_machine(
41                test,
42                traces,
43                source_analysis,
44                config,
45                event_log,
46            );
47            (state, Box::new(machine))
48        }
49    }
50}
51
52#[derive(Debug, Copy, Clone, Eq, PartialEq)]
53pub enum TestState {
54    /// Start state. Wait for test to appear and track time to enable timeout
55    Start { start_time: Instant },
56    /// Initialise: once test process appears instrument
57    Initialise,
58    /// Waiting for breakpoint to be hit or test to end
59    Waiting { start_time: Instant },
60    /// Test process stopped, check coverage
61    Stopped,
62    /// Test exited normally. Includes the exit code of the test executable.
63    End(i32),
64}
65
66/// This enum represents a generic action for the process tracing API to take
67/// along with any form of ID or handle to the underlying thread or process
68/// i.e. a PID in Unix.
69#[derive(Debug, Clone, Eq, PartialEq)]
70pub enum TracerAction<T> {
71    /// Try continue is for times when you don't know if there is something
72    /// paused but if there is you want it to move on.
73    TryContinue(T),
74    Continue(T),
75    Step(T),
76    Detach(T),
77    Nothing,
78}
79
80impl<T> TracerAction<T> {
81    pub fn get_data(&self) -> Option<&T> {
82        match self {
83            TracerAction::Continue(d) => Some(d),
84            TracerAction::Step(d) => Some(d),
85            TracerAction::Detach(d) => Some(d),
86            TracerAction::TryContinue(d) => Some(d),
87            _ => None,
88        }
89    }
90}
91
92/// Tracing a process on an OS will have platform specific code.
93/// Structs containing the platform specific datastructures should
94/// provide this trait with an implementation of the handling of
95/// the given states.
96pub trait StateData {
97    /// Starts the tracing. Returns None while waiting for
98    /// start. Statemachine then checks timeout
99    fn start(&mut self) -> Result<Option<TestState>, RunError>;
100    /// Initialises test for tracing returns next state
101    fn init(&mut self) -> Result<TestState, RunError>;
102    /// Waits for notification from test executable that there's
103    /// something to do. Selects the next appropriate state if there's
104    /// something to do otherwise None
105    fn wait(&mut self) -> Result<Option<TestState>, RunError>;
106    /// This is here for the times when we're about to mark the attempted coverage collection as a
107    /// failure i.e. timeout, but there's an alternative to that which can see if we're actually in
108    /// a "finished" state but are still waiting on resource cleanup so we don't lose the results.
109    fn last_wait_attempt(&mut self) -> Result<Option<TestState>, RunError>;
110    /// Handle a stop in the test executable. Coverage data will
111    /// be collected here as well as other OS specific functions
112    fn stop(&mut self) -> Result<TestState, RunError>;
113}
114
115impl<'a> StateData for Box<dyn StateData + 'a> {
116    fn start(&mut self) -> Result<Option<TestState>, RunError> {
117        self.as_mut().start()
118    }
119
120    fn init(&mut self) -> Result<TestState, RunError> {
121        self.as_mut().init()
122    }
123
124    fn wait(&mut self) -> Result<Option<TestState>, RunError> {
125        self.as_mut().wait()
126    }
127
128    fn last_wait_attempt(&mut self) -> Result<Option<TestState>, RunError> {
129        self.as_mut().last_wait_attempt()
130    }
131
132    fn stop(&mut self) -> Result<TestState, RunError> {
133        self.as_mut().stop()
134    }
135}
136
137impl TestState {
138    /// Convenience function used to check if the test has finished or errored
139    pub fn is_finished(self) -> bool {
140        matches!(self, TestState::End(_))
141    }
142
143    /// Convenience function for creating start states
144    fn start_state() -> TestState {
145        TestState::Start {
146            start_time: Instant::now(),
147        }
148    }
149
150    /// Convenience function for creating wait states
151    fn wait_state() -> TestState {
152        TestState::Waiting {
153            start_time: Instant::now(),
154        }
155    }
156
157    /// Updates the state machine state
158    pub fn step<T: StateData>(self, data: &mut T, config: &Config) -> Result<TestState, RunError> {
159        match self {
160            TestState::Start { start_time } => {
161                if let Some(s) = data.start()? {
162                    Ok(s)
163                } else if start_time.elapsed() >= config.test_timeout {
164                    Err(RunError::TestRuntime(
165                        "Error: Timed out when starting test".to_string(),
166                    ))
167                } else {
168                    Ok(TestState::Start { start_time })
169                }
170            }
171            TestState::Initialise => data.init(),
172            TestState::Waiting { start_time } => {
173                if let Some(s) = data.wait()? {
174                    Ok(s)
175                } else if start_time.elapsed() >= config.test_timeout {
176                    if let Some(s) = data.last_wait_attempt()? {
177                        Ok(s)
178                    } else {
179                        Err(RunError::TestRuntime(
180                            "Error: Timed out waiting for test response".to_string(),
181                        ))
182                    }
183                } else {
184                    Ok(TestState::Waiting { start_time })
185                }
186            }
187            TestState::Stopped => data.stop(),
188            TestState::End(e) => Ok(TestState::End(e)),
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use std::time::Duration;
197
198    impl StateData for () {
199        fn start(&mut self) -> Result<Option<TestState>, RunError> {
200            Ok(None)
201        }
202
203        fn init(&mut self) -> Result<TestState, RunError> {
204            Err(RunError::StateMachine(
205                "No valid coverage collector".to_string(),
206            ))
207        }
208
209        fn wait(&mut self) -> Result<Option<TestState>, RunError> {
210            Ok(None)
211        }
212
213        fn last_wait_attempt(&mut self) -> Result<Option<TestState>, RunError> {
214            Err(RunError::StateMachine(
215                "No valid coverage collector".to_string(),
216            ))
217        }
218        fn stop(&mut self) -> Result<TestState, RunError> {
219            Err(RunError::StateMachine(
220                "No valid coverage collector".to_string(),
221            ))
222        }
223    }
224
225    #[test]
226    fn hits_timeouts() {
227        let mut config = Config::default();
228        config.test_timeout = Duration::from_secs(5);
229
230        let start_time = Instant::now() - Duration::from_secs(6);
231
232        let state = TestState::Start { start_time };
233
234        assert!(state.step(&mut (), &config).is_err());
235
236        let state = TestState::Waiting { start_time };
237
238        assert!(state.step(&mut (), &config).is_err());
239    }
240}