1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
use crate::config::{Config, TraceEngine};
use crate::errors::RunError;
use crate::event_log::*;
use crate::traces::*;
use crate::LineAnalysis;
use crate::TestHandle;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Instant;
use tracing::error;

pub mod instrumented;
cfg_if::cfg_if! {
    if #[cfg(ptrace_supported)] {
        pub mod linux;
        pub use linux::ProcessInfo;
    }
}
pub fn create_state_machine<'a>(
    test: impl Into<TestHandle>,
    traces: &'a mut TraceMap,
    source_analysis: &'a HashMap<PathBuf, LineAnalysis>,
    config: &'a Config,
    event_log: &'a Option<EventLog>,
) -> (TestState, Box<dyn StateData + 'a>) {
    match config.engine() {
        TraceEngine::Ptrace => {
            cfg_if::cfg_if! {
                if #[cfg(ptrace_supported)] {
                    let (state, machine) = linux::create_state_machine(test, traces, source_analysis, config, event_log);
                    (state, Box::new(machine))
                } else {
                    error!("The ptrace backend is not supported on this system");
                    panic!()
                }
            }
        }
        // Should never be auto so ignore our normal rules
        TraceEngine::Llvm | TraceEngine::Auto => {
            let (state, machine) = instrumented::create_state_machine(
                test,
                traces,
                source_analysis,
                config,
                event_log,
            );
            (state, Box::new(machine))
        }
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum TestState {
    /// Start state. Wait for test to appear and track time to enable timeout
    Start { start_time: Instant },
    /// Initialise: once test process appears instrument
    Initialise,
    /// Waiting for breakpoint to be hit or test to end
    Waiting { start_time: Instant },
    /// Test process stopped, check coverage
    Stopped,
    /// Test exited normally. Includes the exit code of the test executable.
    End(i32),
}

/// This enum represents a generic action for the process tracing API to take
/// along with any form of ID or handle to the underlying thread or process
/// i.e. a PID in Unix.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum TracerAction<T> {
    /// Try continue is for times when you don't know if there is something
    /// paused but if there is you want it to move on.
    TryContinue(T),
    Continue(T),
    Step(T),
    Detach(T),
    Nothing,
}

impl<T> TracerAction<T> {
    pub fn get_data(&self) -> Option<&T> {
        match self {
            TracerAction::Continue(d) => Some(d),
            TracerAction::Step(d) => Some(d),
            TracerAction::Detach(d) => Some(d),
            TracerAction::TryContinue(d) => Some(d),
            _ => None,
        }
    }
}

/// Tracing a process on an OS will have platform specific code.
/// Structs containing the platform specific datastructures should
/// provide this trait with an implementation of the handling of
/// the given states.
pub trait StateData {
    /// Starts the tracing. Returns None while waiting for
    /// start. Statemachine then checks timeout
    fn start(&mut self) -> Result<Option<TestState>, RunError>;
    /// Initialises test for tracing returns next state
    fn init(&mut self) -> Result<TestState, RunError>;
    /// Waits for notification from test executable that there's
    /// something to do. Selects the next appropriate state if there's
    /// something to do otherwise None
    fn wait(&mut self) -> Result<Option<TestState>, RunError>;
    /// This is here for the times when we're about to mark the attempted coverage collection as a
    /// failure i.e. timeout, but there's an alternative to that which can see if we're actually in
    /// a "finished" state but are still waiting on resource cleanup so we don't lose the results.
    fn last_wait_attempt(&mut self) -> Result<Option<TestState>, RunError>;
    /// Handle a stop in the test executable. Coverage data will
    /// be collected here as well as other OS specific functions
    fn stop(&mut self) -> Result<TestState, RunError>;
}

impl<'a> StateData for Box<dyn StateData + 'a> {
    fn start(&mut self) -> Result<Option<TestState>, RunError> {
        self.as_mut().start()
    }

    fn init(&mut self) -> Result<TestState, RunError> {
        self.as_mut().init()
    }

    fn wait(&mut self) -> Result<Option<TestState>, RunError> {
        self.as_mut().wait()
    }

    fn last_wait_attempt(&mut self) -> Result<Option<TestState>, RunError> {
        self.as_mut().last_wait_attempt()
    }

    fn stop(&mut self) -> Result<TestState, RunError> {
        self.as_mut().stop()
    }
}

impl TestState {
    /// Convenience function used to check if the test has finished or errored
    pub fn is_finished(self) -> bool {
        matches!(self, TestState::End(_))
    }

    /// Convenience function for creating start states
    fn start_state() -> TestState {
        TestState::Start {
            start_time: Instant::now(),
        }
    }

    /// Convenience function for creating wait states
    fn wait_state() -> TestState {
        TestState::Waiting {
            start_time: Instant::now(),
        }
    }

    /// Updates the state machine state
    pub fn step<T: StateData>(self, data: &mut T, config: &Config) -> Result<TestState, RunError> {
        match self {
            TestState::Start { start_time } => {
                if let Some(s) = data.start()? {
                    Ok(s)
                } else if start_time.elapsed() >= config.test_timeout {
                    Err(RunError::TestRuntime(
                        "Error: Timed out when starting test".to_string(),
                    ))
                } else {
                    Ok(TestState::Start { start_time })
                }
            }
            TestState::Initialise => data.init(),
            TestState::Waiting { start_time } => {
                if let Some(s) = data.wait()? {
                    Ok(s)
                } else if start_time.elapsed() >= config.test_timeout {
                    if let Some(s) = data.last_wait_attempt()? {
                        Ok(s)
                    } else {
                        Err(RunError::TestRuntime(
                            "Error: Timed out waiting for test response".to_string(),
                        ))
                    }
                } else {
                    Ok(TestState::Waiting { start_time })
                }
            }
            TestState::Stopped => data.stop(),
            TestState::End(e) => Ok(TestState::End(e)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;

    impl StateData for () {
        fn start(&mut self) -> Result<Option<TestState>, RunError> {
            Ok(None)
        }

        fn init(&mut self) -> Result<TestState, RunError> {
            Err(RunError::StateMachine(
                "No valid coverage collector".to_string(),
            ))
        }

        fn wait(&mut self) -> Result<Option<TestState>, RunError> {
            Ok(None)
        }

        fn last_wait_attempt(&mut self) -> Result<Option<TestState>, RunError> {
            Err(RunError::StateMachine(
                "No valid coverage collector".to_string(),
            ))
        }
        fn stop(&mut self) -> Result<TestState, RunError> {
            Err(RunError::StateMachine(
                "No valid coverage collector".to_string(),
            ))
        }
    }

    #[test]
    fn hits_timeouts() {
        let mut config = Config::default();
        config.test_timeout = Duration::from_secs(5);

        let start_time = Instant::now() - Duration::from_secs(6);

        let state = TestState::Start { start_time };

        assert!(state.step(&mut (), &config).is_err());

        let state = TestState::Waiting { start_time };

        assert!(state.step(&mut (), &config).is_err());
    }
}