phink_lib/cover/
coverage.rs

1use crate::{
2    cli::config::{
3        PFiles::CoverageTracePath,
4        PhinkFiles,
5    },
6    cover::trace::CoverageTrace,
7};
8use std::{
9    fmt,
10    fmt::{
11        Debug,
12        Formatter,
13    },
14    fs::OpenOptions,
15    hint::black_box,
16    io::Write,
17    path::PathBuf,
18};
19
20// extern "C" {
21//     fn __afl_coverage_interesting(val: u8, id: u32);
22// }
23
24#[derive(Clone, Default)]
25pub struct InputCoverage {
26    /// All the coverage ID grabbed
27    all_cov_id: Vec<u64>,
28    /// Full debug stack trace without parsing
29    trace: Vec<CoverageTrace>,
30}
31
32impl Debug for InputCoverage {
33    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
34        f.debug_struct("Coverage")
35            .field("", &self.all_cov_id)
36            .finish()
37    }
38}
39
40impl InputCoverage {
41    pub fn new() -> InputCoverage {
42        InputCoverage {
43            all_cov_id: Vec::new(),
44            trace: vec![],
45        }
46    }
47    pub fn coverage_len(&self) -> usize {
48        self.all_cov_id.len()
49    }
50
51    pub fn messages_coverage(&self) -> &Vec<u64> {
52        &self.all_cov_id
53    }
54
55    pub fn concatened_trace(&self) -> String {
56        self.trace
57            .iter()
58            .map(|coverage_trace| coverage_trace.as_string())
59            .collect::<Vec<String>>()
60            .join(" | ")
61            .replace("\n", " ")
62    }
63
64    pub fn add_cov(&mut self, coverage: CoverageTrace) {
65        let parsed = coverage.parse_coverage();
66        self.trace.push(coverage);
67        for id in parsed {
68            self.all_cov_id.push(id);
69        }
70    }
71
72    pub fn save(&self, output: &PathBuf) -> std::io::Result<()> {
73        let mut trace_strings: Vec<String> = self
74            .messages_coverage()
75            .iter()
76            .map(|id| id.to_string())
77            .collect();
78
79        trace_strings.sort_unstable();
80
81        let mut file = OpenOptions::new()
82            .append(true)
83            .create(true)
84            .open(PhinkFiles::new_by_ref(output).path(CoverageTracePath))?;
85        // Write each unique ID to the file, one per line
86        writeln!(file, "{}", trace_strings.join("\n"))?;
87        Ok(())
88    }
89
90    /// We assume that the instrumentation will never insert more than
91    /// `1_000` artificial branches This value should be big enough
92    /// to handle most of smart-contract, even the biggest
93    #[allow(clippy::identity_op)]
94    pub fn redirect_coverage(&self, flat: &[u64]) {
95        seq_macro::seq!(cov_id in 0_u64 .. 1_000_u64 {
96            if black_box(flat.contains(&cov_id)) {
97                let cov = black_box(cov_id.saturating_add(1));
98                print!("(DEBUG)-cov:{cov}");
99              }
100        });
101    }
102}
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use tempfile::TempDir;
107
108    #[test]
109    fn test_coverage_len() {
110        let mut coverage = InputCoverage::new();
111        coverage.all_cov_id.push(1);
112        coverage.all_cov_id.push(2);
113        assert_eq!(coverage.coverage_len(), 2);
114    }
115
116    #[test]
117    fn test_messages_coverage() {
118        let mut coverage = InputCoverage::new();
119        coverage.all_cov_id.push(1);
120        coverage.all_cov_id.push(2);
121        let messages = coverage.messages_coverage();
122        assert_eq!(messages.len(), 2);
123        assert!(messages.contains(&1));
124        assert!(messages.contains(&2));
125    }
126
127    #[test]
128    fn test_add_cov() {
129        let mut coverage = InputCoverage::new();
130        let trace = CoverageTrace::from("COV=1 COV=2 COV=3".as_bytes().to_vec());
131        coverage.add_cov(trace);
132        assert_eq!(coverage.coverage_len(), 3);
133        assert!(coverage.messages_coverage().contains(&1));
134        assert!(coverage.messages_coverage().contains(&2));
135        assert!(coverage.messages_coverage().contains(&3));
136    }
137
138    #[test]
139    #[ignore] // we actually want duplicated
140    fn test_add_cov_deduplication() {
141        let mut coverage = InputCoverage::new();
142        let trace1 = CoverageTrace::from("COV=1 COV=2 COV=3".as_bytes().to_vec());
143        let trace2 = CoverageTrace::from("COV=2 COV=3 COV=4".as_bytes().to_vec());
144        coverage.add_cov(trace1);
145        coverage.add_cov(trace2);
146        assert_eq!(coverage.coverage_len(), 4);
147    }
148
149    #[test]
150    fn test_debug_format() {
151        let mut coverage = InputCoverage::new();
152        coverage.all_cov_id.push(1);
153        coverage.all_cov_id.push(2);
154        let debug_output = format!("{:?}", coverage);
155        assert!(debug_output.contains("Coverage"));
156        assert!(debug_output.contains("1"));
157        assert!(debug_output.contains("2"));
158    }
159
160    #[test]
161    fn test_save() {
162        let mut coverage = InputCoverage::new();
163        coverage.all_cov_id.push(1);
164        coverage.all_cov_id.push(2);
165        coverage.all_cov_id.push(3);
166
167        let output = TempDir::new().unwrap().into_path();
168        let cov_trace_path = PhinkFiles::new(output.clone())
169            .make_all()
170            .path(CoverageTracePath);
171
172        coverage.save(&output).unwrap();
173
174        let content = std::fs::read_to_string(cov_trace_path).unwrap();
175        let lines: Vec<String> = content.lines().map(String::from).collect();
176
177        assert_eq!(
178            lines,
179            Vec::from_iter(vec!["1".to_string(), "2".to_string(), "3".to_string()])
180        );
181    }
182}