anchor_coverage/
lib.rs

1use addr2line::Loader;
2use anyhow::{anyhow, Result};
3use byteorder::{LittleEndian, ReadBytesExt};
4use cargo_metadata::MetadataCommand;
5use std::{
6    collections::BTreeMap,
7    env::var_os,
8    fs::{metadata, File, OpenOptions},
9    io::Write,
10    path::{Path, PathBuf},
11};
12
13pub const DOCKER_BUILDER_VERSION: &str = "0.0.0";
14
15/// Default RPC port
16pub const DEFAULT_RPC_PORT: u16 = 8899;
17
18#[cfg(feature = "__anchor_cli")]
19mod anchor_cli_lib;
20#[cfg(feature = "__anchor_cli")]
21pub use anchor_cli_lib::__build_with_debug;
22#[cfg(feature = "__anchor_cli")]
23pub use anchor_cli_lib::{
24    __get_keypair as get_keypair, __is_hidden as is_hidden, __keys_sync as keys_sync,
25};
26
27#[cfg(feature = "__anchor_cli")]
28mod anchor_cli_config;
29#[cfg(feature = "__anchor_cli")]
30use anchor_cli_config as config;
31#[cfg(feature = "__anchor_cli")]
32pub use anchor_cli_config::{BootstrapMode, ConfigOverride, ProgramArch};
33
34mod insn;
35use insn::Insn;
36
37mod start_address;
38use start_address::start_address;
39
40pub mod util;
41use util::{files_with_extension, StripCurrentDir};
42
43mod vaddr;
44use vaddr::Vaddr;
45
46#[derive(Clone, Debug, Default, Eq, PartialEq)]
47struct Entry<'a> {
48    file: &'a str,
49    line: u32,
50}
51
52struct Dwarf {
53    path: PathBuf,
54    start_address: u64,
55    #[allow(dead_code, reason = "`vaddr` points into `loader`")]
56    loader: &'static Loader,
57    vaddr_entry_map: BTreeMap<u64, Entry<'static>>,
58}
59
60enum Outcome {
61    Lcov(PathBuf),
62    ClosestMatch(PathBuf),
63}
64
65type Vaddrs = Vec<u64>;
66
67type VaddrEntryMap<'a> = BTreeMap<u64, Entry<'a>>;
68
69#[allow(dead_code)]
70#[derive(Debug)]
71struct ClosestMatch<'a, 'b> {
72    pcs_path: &'a Path,
73    debug_path: &'b Path,
74    mismatch: Mismatch,
75}
76
77#[allow(dead_code)]
78#[derive(Clone, Copy, Debug, Default)]
79struct Mismatch {
80    index: usize,
81    vaddr: Vaddr,
82    expected: Insn,
83    actual: Insn,
84}
85
86type FileLineCountMap<'a> = BTreeMap<&'a str, BTreeMap<u32, usize>>;
87
88pub fn run(sbf_trace_dir: impl AsRef<Path>, debug: bool) -> Result<()> {
89    let mut lcov_paths = Vec::new();
90    let mut closest_match_paths = Vec::new();
91
92    let debug_paths = debug_paths()?;
93
94    let dwarfs = debug_paths
95        .into_iter()
96        .map(|path| build_dwarf(&path))
97        .collect::<Result<Vec<_>>>()?;
98
99    if dwarfs.is_empty() {
100        eprintln!("Found no debug files");
101        return Ok(());
102    }
103
104    if debug {
105        for dwarf in dwarfs {
106            dump_vaddr_entry_map(dwarf.vaddr_entry_map);
107        }
108        return Ok(());
109    }
110
111    let pcs_paths = files_with_extension(&sbf_trace_dir, "pcs")?;
112
113    for pcs_path in &pcs_paths {
114        match process_pcs_path(&dwarfs, pcs_path)? {
115            Outcome::Lcov(lcov_path) => {
116                lcov_paths.push(lcov_path.strip_current_dir().to_path_buf());
117            }
118            Outcome::ClosestMatch(closest_match_path) => {
119                closest_match_paths.push(closest_match_path.strip_current_dir().to_path_buf());
120            }
121        }
122    }
123
124    eprintln!(
125        "
126Processed {} of {} program counter files
127
128Lcov files written: {lcov_paths:#?}
129
130Closest match files written: {closest_match_paths:#?}
131
132If you are done generating lcov files, try running:
133
134    genhtml --output-directory coverage {}/*.lcov && open coverage/index.html
135",
136        lcov_paths.len(),
137        pcs_paths.len(),
138        sbf_trace_dir.as_ref().strip_current_dir().display()
139    );
140
141    Ok(())
142}
143
144fn debug_paths() -> Result<Vec<PathBuf>> {
145    let metadata = MetadataCommand::new().no_deps().exec()?;
146    let target_directory = metadata.target_directory;
147    files_with_extension(target_directory.join("deploy"), "debug")
148}
149
150fn build_dwarf(debug_path: &Path) -> Result<Dwarf> {
151    let start_address = start_address(debug_path)?;
152
153    let loader = Loader::new(debug_path).map_err(|error| {
154        anyhow!(
155            "failed to build loader for {}: {}",
156            debug_path.display(),
157            error
158        )
159    })?;
160
161    let loader = Box::leak(Box::new(loader));
162
163    let vaddr_entry_map = build_vaddr_entry_map(loader, debug_path)?;
164
165    Ok(Dwarf {
166        path: debug_path.to_path_buf(),
167        start_address,
168        loader,
169        vaddr_entry_map,
170    })
171}
172
173fn process_pcs_path(dwarfs: &[Dwarf], pcs_path: &Path) -> Result<Outcome> {
174    eprintln!();
175    eprintln!(
176        "Program counters file: {}",
177        pcs_path.strip_current_dir().display()
178    );
179
180    let mut vaddrs = read_vaddrs(pcs_path)?;
181
182    eprintln!("Program counters read: {}", vaddrs.len());
183
184    let (dwarf, mismatch) = find_applicable_dwarf(dwarfs, pcs_path, &mut vaddrs)?;
185
186    if let Some(mismatch) = mismatch {
187        return write_closest_match(pcs_path, dwarf, mismatch).map(Outcome::ClosestMatch);
188    }
189
190    eprintln!(
191        "Applicable dwarf: {}",
192        dwarf.path.strip_current_dir().display()
193    );
194
195    assert!(vaddrs
196        .first()
197        .is_some_and(|&vaddr| vaddr == dwarf.start_address));
198
199    // smoelius: If a sequence of program counters refer to the same file and line, treat them as
200    // one hit to that file and line.
201    vaddrs.dedup_by_key::<_, Option<&Entry>>(|vaddr| dwarf.vaddr_entry_map.get(vaddr));
202
203    // smoelius: A `vaddr` could not have an entry because its file does not exist. Keep only those
204    // `vaddr`s that have entries.
205    let vaddrs = vaddrs
206        .into_iter()
207        .filter(|vaddr| dwarf.vaddr_entry_map.contains_key(vaddr))
208        .collect::<Vec<_>>();
209
210    eprintln!("Line hits: {}", vaddrs.len());
211
212    let file_line_count_map = build_file_line_count_map(&dwarf.vaddr_entry_map, vaddrs);
213
214    write_lcov_file(pcs_path, file_line_count_map).map(Outcome::Lcov)
215}
216
217static CARGO_HOME: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| {
218    if let Some(cargo_home) = var_os("CARGO_HOME") {
219        PathBuf::from(cargo_home)
220    } else {
221        #[allow(deprecated)]
222        #[cfg_attr(
223            dylint_lib = "inconsistent_qualification",
224            allow(inconsistent_qualification)
225        )]
226        std::env::home_dir().unwrap().join(".cargo")
227    }
228});
229
230fn build_vaddr_entry_map<'a>(loader: &'a Loader, debug_path: &Path) -> Result<VaddrEntryMap<'a>> {
231    let mut vaddr_entry_map = VaddrEntryMap::new();
232    let metadata = metadata(debug_path)?;
233    for vaddr in (0..metadata.len()).step_by(size_of::<u64>()) {
234        let location = loader
235            .find_location(vaddr)
236            .map_err(|error| anyhow!("failed to find location for address 0x{vaddr:x}: {error}"))?;
237        let Some(location) = location else {
238            continue;
239        };
240        let Some(file) = location.file else {
241            continue;
242        };
243        // smoelius: Ignore files that do not exist.
244        if !Path::new(file).try_exists()? {
245            continue;
246        }
247        if !include_cargo() && file.starts_with(CARGO_HOME.to_string_lossy().as_ref()) {
248            continue;
249        }
250        let Some(line) = location.line else {
251            continue;
252        };
253        // smoelius: Even though we ignore columns, fetch them should we ever want to act on them.
254        let Some(_column) = location.column else {
255            continue;
256        };
257        let entry = vaddr_entry_map.entry(vaddr).or_default();
258        entry.file = file;
259        entry.line = line;
260    }
261    Ok(vaddr_entry_map)
262}
263
264fn dump_vaddr_entry_map(vaddr_entry_map: BTreeMap<u64, Entry<'_>>) {
265    let mut prev = String::new();
266    for (vaddr, Entry { file, line }) in vaddr_entry_map {
267        let curr = format!("{file}:{line}");
268        if prev != curr {
269            eprintln!("0x{vaddr:x}: {curr}");
270            prev = curr;
271        }
272    }
273}
274
275fn read_vaddrs(pcs_path: &Path) -> Result<Vaddrs> {
276    let mut vaddrs = Vaddrs::new();
277    let mut pcs_file = File::open(pcs_path)?;
278    while let Ok(pc) = pcs_file.read_u64::<LittleEndian>() {
279        let vaddr = pc << 3;
280        vaddrs.push(vaddr);
281    }
282    Ok(vaddrs)
283}
284
285fn find_applicable_dwarf<'a>(
286    dwarfs: &'a [Dwarf],
287    pcs_path: &Path,
288    vaddrs: &mut [u64],
289) -> Result<(&'a Dwarf, Option<Mismatch>)> {
290    let dwarf_mismatches = collect_dwarf_mismatches(dwarfs, pcs_path, vaddrs)?;
291
292    if let Some((dwarf, _)) = dwarf_mismatches
293        .iter()
294        .find(|(_, mismatch)| mismatch.is_none())
295    {
296        let vaddr_first = *vaddrs.first().unwrap();
297
298        assert!(dwarf.start_address >= vaddr_first);
299
300        let shift = dwarf.start_address - vaddr_first;
301
302        // smoelius: Make the shift "permanent".
303        for vaddr in vaddrs.iter_mut() {
304            *vaddr += shift;
305        }
306
307        return Ok((dwarf, None));
308    }
309
310    Ok(dwarf_mismatches
311        .into_iter()
312        .max_by_key(|(_, mismatch)| mismatch.as_ref().unwrap().index)
313        .unwrap())
314}
315
316fn collect_dwarf_mismatches<'a>(
317    dwarfs: &'a [Dwarf],
318    pcs_path: &Path,
319    vaddrs: &[u64],
320) -> Result<Vec<(&'a Dwarf, Option<Mismatch>)>> {
321    dwarfs
322        .iter()
323        .map(|dwarf| {
324            let mismatch = dwarf_mismatch(vaddrs, dwarf, pcs_path)?;
325            Ok((dwarf, mismatch))
326        })
327        .collect()
328}
329
330fn dwarf_mismatch(vaddrs: &[u64], dwarf: &Dwarf, pcs_path: &Path) -> Result<Option<Mismatch>> {
331    use std::io::{Seek, SeekFrom};
332
333    let Some(&vaddr_first) = vaddrs.first() else {
334        return Ok(Some(Mismatch::default()));
335    };
336
337    if dwarf.start_address < vaddr_first {
338        return Ok(Some(Mismatch::default()));
339    }
340
341    // smoelius: `start_address` is both an offset into the ELF file and a virtual address. The
342    // current virtual addresses are offsets from the start of the text section. The current virtual
343    // addresses must be shifted so that the first matches the start address.
344    let shift = dwarf.start_address - vaddr_first;
345
346    let mut so_file = File::open(dwarf.path.with_extension("so"))?;
347    let mut insns_file = File::open(pcs_path.with_extension("insns"))?;
348
349    for (index, &vaddr) in vaddrs.iter().enumerate() {
350        let vaddr = vaddr + shift;
351
352        so_file.seek(SeekFrom::Start(vaddr))?;
353        let expected = so_file.read_u64::<LittleEndian>()?;
354
355        let actual = insns_file.read_u64::<LittleEndian>()?;
356
357        // smoelius: 0x85 is a function call. That they would be patched and differ is not
358        // surprising.
359        if expected & 0xff == 0x85 {
360            continue;
361        }
362
363        if expected != actual {
364            return Ok(Some(Mismatch {
365                index,
366                vaddr: Vaddr::from(vaddr),
367                expected: Insn::from(expected),
368                actual: Insn::from(actual),
369            }));
370        }
371    }
372
373    Ok(None)
374}
375
376fn write_closest_match(pcs_path: &Path, dwarf: &Dwarf, mismatch: Mismatch) -> Result<PathBuf> {
377    let closest_match_path = pcs_path.with_extension("closest_match");
378    let mut file = OpenOptions::new()
379        .create(true)
380        .truncate(true)
381        .write(true)
382        .open(&closest_match_path)?;
383    writeln!(
384        file,
385        "{:#?}",
386        ClosestMatch {
387            pcs_path,
388            debug_path: &dwarf.path,
389            mismatch
390        }
391    )?;
392    Ok(closest_match_path)
393}
394
395fn build_file_line_count_map<'a>(
396    vaddr_entry_map: &BTreeMap<u64, Entry<'a>>,
397    vaddrs: Vaddrs,
398) -> FileLineCountMap<'a> {
399    let mut file_line_count_map = FileLineCountMap::new();
400    for Entry { file, line } in vaddr_entry_map.values() {
401        let line_count_map = file_line_count_map.entry(file).or_default();
402        line_count_map.insert(*line, 0);
403    }
404
405    for vaddr in vaddrs {
406        // smoelius: A `vaddr` could not have an entry because its file does not exist.
407        let Some(entry) = vaddr_entry_map.get(&vaddr) else {
408            continue;
409        };
410        let line_count_map = file_line_count_map.get_mut(entry.file).unwrap();
411        let count = line_count_map.get_mut(&entry.line).unwrap();
412        *count += 1;
413    }
414
415    file_line_count_map
416}
417
418fn write_lcov_file(pcs_path: &Path, file_line_count_map: FileLineCountMap<'_>) -> Result<PathBuf> {
419    let lcov_path = Path::new(pcs_path).with_extension("lcov");
420
421    let mut file = OpenOptions::new()
422        .create(true)
423        .truncate(true)
424        .write(true)
425        .open(&lcov_path)?;
426
427    for (source_file, line_count_map) in file_line_count_map {
428        // smoelius: Stripping `current_dir` from `source_file` has not effect on what's displayed.
429        writeln!(file, "SF:{source_file}")?;
430        for (line, count) in line_count_map {
431            writeln!(file, "DA:{line},{count}")?;
432        }
433        writeln!(file, "end_of_record")?;
434    }
435
436    Ok(lcov_path)
437}
438
439fn include_cargo() -> bool {
440    var_os("INCLUDE_CARGO").is_some()
441}
442
443#[cfg(test)]
444mod tests;
445
446#[test]
447fn nested_workspace() {
448    nested_workspace::test().unwrap();
449}