opentitanlib/io/console/
coverage.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::VecDeque;
6use std::fs;
7use std::sync::Mutex;
8use std::task::{Context, Poll, ready};
9
10use anyhow::{Result, bail};
11use crc::{CRC_32_ISO_HDLC, Crc};
12
13use super::{ConsoleDevice, CoverageConsole};
14use crate::io::middleware::{Middleware, UartMiddleware};
15use crate::io::uart::Uart;
16
17const COVERAGE_START_ANCHOR: &[u8] = b"== COVERAGE PROFILE START ==\r\n";
18const COVERAGE_END_ANCHOR: &[u8] = b"== COVERAGE PROFILE END ==\r\n";
19const COVERAGE_SKIP_ANCHOR: &[u8] = b"== COVERAGE PROFILE SKIP ==\r\n";
20
21#[derive(Default, PartialEq, Clone, Copy)]
22enum BufferMode {
23    #[default]
24    Normal,
25    Coverage,
26}
27
28pub struct CoverageMiddleware<T> {
29    inner: T,
30    profile_path_template: Option<String>,
31    state: Mutex<CoverageState>,
32}
33
34struct CoverageState {
35    mode: BufferMode,
36    /// Buffer for matching anchors.
37    match_buf: VecDeque<u8>,
38    /// Buffer for confirmed normal data ready to be returned to the user.
39    output_buf: VecDeque<u8>,
40    /// Buffer for coverage data (hex encoded).
41    coverage_buf: String,
42    /// Number of coverage blocks processed (finished or skipped).
43    blocks_processed: usize,
44}
45
46impl Default for CoverageState {
47    fn default() -> Self {
48        Self {
49            mode: BufferMode::Normal,
50            match_buf: VecDeque::new(),
51            output_buf: VecDeque::new(),
52            coverage_buf: String::new(),
53            blocks_processed: 0,
54        }
55    }
56}
57
58impl<T> CoverageMiddleware<T> {
59    pub fn new(inner: T) -> Self {
60        Self {
61            inner,
62            profile_path_template: None,
63            state: Mutex::new(CoverageState::default()),
64        }
65    }
66
67    #[cfg(test)]
68    pub fn new_with_template(inner: T, template: String) -> Self {
69        Self {
70            inner,
71            profile_path_template: Some(template),
72            state: Mutex::new(CoverageState::default()),
73        }
74    }
75
76    fn starts_with_any_anchor(data: &[u8]) -> Option<&'static [u8]> {
77        if data.starts_with(COVERAGE_START_ANCHOR) {
78            return Some(COVERAGE_START_ANCHOR);
79        }
80        if data.starts_with(COVERAGE_END_ANCHOR) {
81            return Some(COVERAGE_END_ANCHOR);
82        }
83        if data.starts_with(COVERAGE_SKIP_ANCHOR) {
84            return Some(COVERAGE_SKIP_ANCHOR);
85        }
86        None
87    }
88
89    fn is_any_anchor_prefix(data: &[u8]) -> bool {
90        !data.is_empty()
91            && (COVERAGE_START_ANCHOR.starts_with(data)
92                || COVERAGE_END_ANCHOR.starts_with(data)
93                || COVERAGE_SKIP_ANCHOR.starts_with(data))
94    }
95
96    fn process_coverage_data(&self, coverage_buf: &str) -> Result<()> {
97        let response = hex::decode(coverage_buf)?;
98        if response.len() < 4 {
99            bail!(
100                "Coverage from the device is too short: {} bytes",
101                response.len()
102            );
103        }
104
105        let (response, crc) = response.split_at(response.len() - 4);
106        let crc = u32::from_le_bytes(crc.try_into()?);
107        let actual = Crc::<u32>::new(&CRC_32_ISO_HDLC).checksum(response);
108        if crc != actual {
109            bail!("Coverage corrupted: crc = {crc:08x}, actual = {actual:08x}");
110        }
111
112        let template = self
113            .profile_path_template
114            .clone()
115            .or_else(|| std::env::var("LLVM_PROFILE_FILE").ok())
116            .unwrap_or_else(|| "./default.%p.profraw".to_owned());
117
118        // By default, Bazel 8 uses `%h-%p-%m.profraw`.
119        // %h, %p and %m are standard LLVM profile file placeholders:
120        //   - %h: hostname hardcoded as "test.on.device"
121        //   - %p: process ID replaced with a random number
122        //   - %m: merge pool hardcoded as "0"
123        // Since we are running on a device and saving the profile on the host,
124        // we don't have a meaningful values for those placeholders.
125        let path = template.replace("%h", "test.on.device");
126        let path = path.replace("%p", &rand::random::<u32>().to_string());
127        let path = path.replace("%m", "0");
128        let path = path.replace(".profraw", ".xprofraw");
129
130        log::info!("Saving coverage profile to {}", path);
131        fs::write(path, response)?;
132
133        Ok(())
134    }
135
136    fn handle_anchor(
137        &self,
138        state: &mut CoverageState,
139        anchor: &[u8],
140        cx: &mut Context<'_>,
141    ) -> Option<Poll<Result<usize>>> {
142        for _ in 0..anchor.len() {
143            state.match_buf.pop_front();
144        }
145
146        if state.mode == BufferMode::Normal {
147            if anchor == COVERAGE_START_ANCHOR {
148                state.mode = BufferMode::Coverage;
149                state.coverage_buf.clear();
150            } else if anchor == COVERAGE_END_ANCHOR {
151                log::warn!("Got unexpected coverage end indicator!");
152                state.blocks_processed += 1;
153                cx.waker().wake_by_ref();
154                return Some(Poll::Pending);
155            } else if anchor == COVERAGE_SKIP_ANCHOR {
156                state.blocks_processed += 1;
157                cx.waker().wake_by_ref();
158                return Some(Poll::Pending);
159            }
160        } else {
161            // Coverage mode
162            if anchor == COVERAGE_START_ANCHOR {
163                log::warn!("Got unterminated coverage block:\n{}", state.coverage_buf);
164                state.coverage_buf.clear();
165            } else if anchor == COVERAGE_END_ANCHOR {
166                if let Err(e) = self.process_coverage_data(&state.coverage_buf) {
167                    log::warn!("Failed to process coverage data: {:?}", e);
168                }
169                state.mode = BufferMode::Normal;
170                state.coverage_buf.clear();
171                state.blocks_processed += 1;
172                cx.waker().wake_by_ref();
173                return Some(Poll::Pending);
174            } else if anchor == COVERAGE_SKIP_ANCHOR {
175                log::warn!("Got unterminated coverage block:\n{}", state.coverage_buf);
176                state.mode = BufferMode::Normal;
177                state.coverage_buf.clear();
178                state.blocks_processed += 1;
179                cx.waker().wake_by_ref();
180                return Some(Poll::Pending);
181            }
182        }
183        None
184    }
185
186    fn flush_match_buf(&self, state: &mut CoverageState, len: usize) {
187        for _ in 0..len {
188            let ch = state.match_buf.pop_front().unwrap();
189            if state.mode == BufferMode::Normal {
190                state.output_buf.push_back(ch);
191            } else if ch.is_ascii_hexdigit() {
192                state.coverage_buf.push(ch as char);
193            } else {
194                log::warn!(
195                    "Got invalid hex character '{}' in coverage block. Exiting coverage mode.",
196                    (ch as char).escape_default()
197                );
198                state.mode = BufferMode::Normal;
199                state.coverage_buf.clear();
200                state.output_buf.push_back(ch);
201                state.blocks_processed += 1;
202            }
203        }
204    }
205}
206
207impl<T: ConsoleDevice> Middleware for CoverageMiddleware<T> {
208    type Inner = T;
209    fn inner(&self) -> &T {
210        &self.inner
211    }
212}
213
214impl<T: Uart> UartMiddleware for CoverageMiddleware<T> {
215    fn clear_rx_buffer_impl(&self) -> Result<()> {
216        let mut state = self.state.lock().unwrap();
217        *state = CoverageState::default();
218        self.inner.clear_rx_buffer()
219    }
220}
221
222impl<T: ConsoleDevice> ConsoleDevice for CoverageMiddleware<T> {
223    fn poll_read(&self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<Result<usize>> {
224        // If the inner device already supports coverage, this middleware is a no-op.
225        if self.inner.as_coverage_console().is_some() {
226            return self.inner.poll_read(cx, buf);
227        }
228
229        if buf.is_empty() {
230            return Poll::Ready(Ok(0));
231        }
232        let mut state = self.state.lock().unwrap();
233
234        loop {
235            // First, satisfy from output_buf if possible.
236            if !state.output_buf.is_empty() {
237                let len = std::cmp::min(buf.len(), state.output_buf.len());
238                for b in buf.iter_mut().take(len) {
239                    *b = state.output_buf.pop_front().unwrap();
240                }
241                return Poll::Ready(Ok(len));
242            }
243
244            // output_buf is empty, try to move data from match_buf.
245            if !state.match_buf.is_empty() {
246                let match_buf_flat = state.match_buf.make_contiguous();
247
248                if let Some(anchor) = Self::starts_with_any_anchor(match_buf_flat) {
249                    if let Some(p) = self.handle_anchor(&mut state, anchor, cx) {
250                        return p;
251                    }
252                    continue;
253                }
254
255                // Not starting with an anchor. Find safe portion of match_buf.
256                let mut safe_len = 0;
257                while safe_len < match_buf_flat.len() {
258                    if Self::is_any_anchor_prefix(&match_buf_flat[safe_len..]) {
259                        break;
260                    }
261                    safe_len += 1;
262                }
263
264                if safe_len > 0 {
265                    self.flush_match_buf(&mut state, safe_len);
266                    continue;
267                }
268                // match_buf starts with a prefix, must wait for more data.
269            }
270
271            // Need more data from inner. Read 1 byte.
272            let mut one_byte = [0u8; 1];
273            drop(state);
274            let res = ready!(self.inner.poll_read(cx, &mut one_byte));
275            state = self.state.lock().unwrap();
276
277            match res {
278                Ok(0) => {
279                    // EOF. Flush match_buf.
280                    if !state.match_buf.is_empty() {
281                        let safe_len = state.match_buf.len();
282                        self.flush_match_buf(&mut state, safe_len);
283                    }
284                    return Poll::Ready(Ok(0));
285                }
286                Ok(1) => {
287                    state.match_buf.push_back(one_byte[0]);
288                }
289                _ => unreachable!(),
290            }
291        }
292    }
293
294    fn write(&self, buf: &[u8]) -> Result<()> {
295        self.inner.write(buf)
296    }
297
298    fn as_coverage_console(&self) -> Option<&dyn CoverageConsole> {
299        self.inner.as_coverage_console().or(Some(self))
300    }
301}
302
303impl<T: ConsoleDevice> CoverageConsole for CoverageMiddleware<T> {
304    fn coverage_blocks_processed(&self) -> usize {
305        self.state.lock().unwrap().blocks_processed
306    }
307}