cargo_tarpaulin/
test_loader.rs

1use crate::config::{types::TraceEngine, Config};
2use crate::path_utils::{fix_unc_path, is_coverable_file_path};
3use crate::source_analysis::*;
4use crate::traces::*;
5use gimli::*;
6use object::{read::ObjectSection, Object};
7use rustc_demangle::demangle;
8use std::collections::{HashMap, HashSet};
9use std::fs::File;
10use std::io;
11use std::path::{Path, PathBuf};
12use tracing::{debug, error, trace, warn};
13
14/// Describes a function as `low_pc`, `high_pc` and bool representing `is_test`.
15type FuncDesc = (u64, u64, FunctionType, Option<String>);
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18enum FunctionType {
19    Generated,
20    Test,
21    Standard,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
25pub enum LineType {
26    /// Generated test main. Shouldn't be traced.
27    TestMain,
28    /// Entry of function known to be a test
29    TestEntry(u64),
30    /// Entry of function. May or may not be test
31    FunctionEntry(u64),
32    /// Standard statement
33    Statement,
34    /// Condition
35    Condition,
36    /// Unknown type
37    Unknown,
38    /// Unused meta-code
39    UnusedGeneric,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43struct SourceLocation {
44    pub path: PathBuf,
45    pub line: u64,
46}
47
48impl From<(PathBuf, usize)> for SourceLocation {
49    fn from(other: (PathBuf, usize)) -> Self {
50        Self {
51            path: other.0,
52            line: other.1 as u64,
53        }
54    }
55}
56
57#[derive(Debug, Clone)]
58pub struct TracerData {
59    /// Currently used to find generated __test::main and remove from coverage,
60    /// may have uses in future for finding conditions etc
61    pub trace_type: LineType,
62    /// Start address of the line
63    pub address: Option<u64>,
64    /// Length of the instruction
65    pub length: u64,
66    /// Function name
67    pub fn_name: Option<String>,
68}
69
70fn generate_func_desc<R, Offset>(
71    die: &DebuggingInformationEntry<R, Offset>,
72    debug_str: &DebugStr<R>,
73) -> Result<FuncDesc>
74where
75    R: Reader<Offset = Offset>,
76    Offset: ReaderOffset,
77{
78    let mut func_type = FunctionType::Standard;
79    let low = die.attr_value(DW_AT_low_pc)?;
80    let high = die.attr_value(DW_AT_high_pc)?;
81    let linkage = die.attr_value(DW_AT_linkage_name)?;
82    let fn_name = die.attr_value(DW_AT_name)?;
83
84    let fn_name: Option<String> = match fn_name {
85        Some(AttributeValue::DebugStrRef(offset)) => debug_str
86            .get_str(offset)
87            .and_then(|r| r.to_string().map(|s| s.to_string()))
88            .ok()
89            .map(|r| demangle(r.as_ref()).to_string()),
90        _ => None,
91    };
92
93    // Low is a program counter address so stored in an Addr
94    let low = match low {
95        Some(AttributeValue::Addr(x)) => x,
96        _ => 0u64,
97    };
98    // High is an offset from the base pc, therefore is u64 data.
99    let high = match high {
100        Some(AttributeValue::Udata(x)) => x,
101        _ => 0u64,
102    };
103    if let Some(AttributeValue::DebugStrRef(offset)) = linkage {
104        let name = debug_str
105            .get_str(offset)
106            .and_then(|r| r.to_string().map(|s| s.to_string()))
107            .unwrap_or_else(|_| "".into());
108        let name = demangle(name.as_ref()).to_string();
109        // Simplest test is whether it's in tests namespace.
110        // Rust guidelines recommend all tests are in a tests module.
111        func_type = if name.contains("tests::") {
112            FunctionType::Test
113        } else if name.contains("__test::main") {
114            FunctionType::Generated
115        } else {
116            FunctionType::Standard
117        };
118    }
119    Ok((low, high, func_type, fn_name))
120}
121
122/// Finds all function entry points and returns a vector
123/// This will identify definite tests, but may be prone to false negatives.
124fn get_entry_points<R, Offset>(
125    debug_info: &UnitHeader<R, Offset>,
126    debug_abbrev: &Abbreviations,
127    debug_str: &DebugStr<R>,
128) -> Vec<FuncDesc>
129where
130    R: Reader<Offset = Offset>,
131    Offset: ReaderOffset,
132{
133    let mut result: Vec<FuncDesc> = Vec::new();
134    let mut cursor = debug_info.entries(debug_abbrev);
135    // skip compilation unit root.
136    let _ = cursor.next_entry();
137    while let Ok(Some((_, node))) = cursor.next_dfs() {
138        // Function DIE
139        if node.tag() == DW_TAG_subprogram {
140            if let Ok(fd) = generate_func_desc(node, debug_str) {
141                result.push(fd);
142            }
143        }
144    }
145    result
146}
147
148fn get_addresses_from_program<R, Offset>(
149    prog: IncompleteLineProgram<R>,
150    debug_strs: &DebugStr<R>,
151    entries: &[(u64, LineType, &Option<String>)],
152    config: &Config,
153    result: &mut HashMap<SourceLocation, Vec<TracerData>>,
154) -> Result<()>
155where
156    R: Reader<Offset = Offset>,
157    Offset: ReaderOffset,
158{
159    let project = config.root();
160    let get_string = |x: R| x.to_string().map(|y| y.to_string()).ok();
161    let (cprog, seq) = prog.sequences()?;
162    for s in seq {
163        let mut sm = cprog.resume_from(&s);
164        while let Ok(Some((header, &ln_row))) = sm.next_row() {
165            if ln_row.end_sequence() {
166                break;
167            }
168            // If this row isn't useful move on
169            if !ln_row.is_stmt() || ln_row.line().is_none() {
170                continue;
171            }
172            if let Some(file) = ln_row.file(header) {
173                let mut path = project.clone();
174                if let Some(dir) = file.directory(header) {
175                    if let Some(temp) = dir.string_value(debug_strs).and_then(get_string) {
176                        path.push(temp);
177                    }
178                }
179                if let Ok(p) = path.canonicalize() {
180                    path = fix_unc_path(&p);
181                }
182                let file = file.path_name();
183                let line = ln_row.line().unwrap();
184                if let Some(file) = file.string_value(debug_strs).and_then(get_string) {
185                    path.push(file);
186                    if !path.is_file() {
187                        // Not really a source file!
188                        continue;
189                    }
190                    if is_coverable_file_path(&path, &project, &config.target_dir()) {
191                        let address = ln_row.address();
192                        let (desc, fn_name) = entries
193                            .iter()
194                            .filter(|&&(addr, _, _)| addr == address)
195                            .map(|&(_, t, fn_name)| (t, fn_name.clone()))
196                            .next()
197                            .unwrap_or((LineType::Unknown, None));
198                        let loc = SourceLocation {
199                            path,
200                            line: line.into(),
201                        };
202                        if desc != LineType::TestMain {
203                            let trace = TracerData {
204                                address: Some(address),
205                                trace_type: desc,
206                                length: 1,
207                                fn_name,
208                            };
209                            let tracerdata = result.entry(loc).or_default();
210                            tracerdata.push(trace);
211                        }
212                    }
213                }
214            }
215        }
216    }
217    Ok(())
218}
219
220fn get_line_addresses<'data>(
221    endian: RunTimeEndian,
222    obj: &'data impl object::read::Object<'data>,
223    analysis: &HashMap<PathBuf, LineAnalysis>,
224    config: &Config,
225) -> Result<TraceMap> {
226    let project = config.root();
227    let io_err = |e| {
228        error!("IO error parsing section: {e}");
229        Error::Io
230    };
231    trace!("Reading object sections");
232    let mut result = TraceMap::new();
233    trace!("Reading .debug_info");
234    let debug_info = obj.section_by_name(".debug_info").ok_or(Error::Io)?;
235    let debug_info = DebugInfo::new(debug_info.data().map_err(io_err)?, endian);
236    trace!("Reading .debug_abbrev");
237    let debug_abbrev = obj.section_by_name(".debug_abbrev").ok_or(Error::Io)?;
238    let debug_abbrev = DebugAbbrev::new(debug_abbrev.data().map_err(io_err)?, endian);
239    trace!("Reading .debug_str");
240    let debug_strings = obj.section_by_name(".debug_str").ok_or(Error::Io)?;
241    let debug_strings = DebugStr::new(debug_strings.data().map_err(io_err)?, endian);
242    trace!("Reading .debug_line");
243    let debug_line = obj.section_by_name(".debug_line").ok_or(Error::Io)?;
244    let debug_line = DebugLine::new(debug_line.data().map_err(io_err)?, endian);
245
246    trace!("Reading .text");
247    let base_addr = obj.section_by_name(".text").ok_or(Error::Io)?;
248
249    trace!("Reading DebugInfo units");
250    let mut iter = debug_info.units();
251    while let Ok(Some(cu)) = iter.next() {
252        let addr_size = cu.address_size();
253        let abbr = match cu.abbreviations(&debug_abbrev) {
254            Ok(a) => a,
255            _ => continue,
256        };
257        let entry_points = get_entry_points(&cu, &abbr, &debug_strings);
258        let entries = entry_points
259            .iter()
260            .map(|(a, b, c, fn_name)| match c {
261                FunctionType::Test => (*a, LineType::TestEntry(*b), fn_name),
262                FunctionType::Standard => (*a, LineType::FunctionEntry(*b), fn_name),
263                FunctionType::Generated => (*a, LineType::TestMain, fn_name),
264            })
265            .collect::<Vec<_>>();
266
267        if let Ok(Some((_, root))) = cu.entries(&abbr).next_dfs() {
268            let offset = match root.attr_value(DW_AT_stmt_list) {
269                Ok(Some(AttributeValue::DebugLineRef(o))) => o,
270                _ => continue,
271            };
272            let prog = debug_line.program(offset, addr_size, None, None)?; // Here?
273            let mut temp_map: HashMap<SourceLocation, Vec<TracerData>> = HashMap::new();
274
275            if let Err(e) =
276                get_addresses_from_program(prog, &debug_strings, &entries, config, &mut temp_map)
277            {
278                debug!("Potential issue reading test addresses {}", e);
279            } else {
280                // Deduplicate addresses
281                for v in temp_map.values_mut() {
282                    v.dedup_by_key(|x| x.address);
283                }
284                let temp_map = temp_map
285                    .into_iter()
286                    .filter(|(ref k, _)| {
287                        config.include_tests() || !k.path.starts_with(project.join("tests"))
288                    })
289                    .filter(|(ref k, _)| !(config.exclude_path(&k.path)))
290                    .filter(|(ref k, _)| config.include_path(&k.path))
291                    .filter(|(ref k, _)| {
292                        !analysis.should_ignore(k.path.as_ref(), &(k.line as usize))
293                    })
294                    .map(|(k, v)| {
295                        let ret = analysis.normalise(k.path.as_ref(), k.line as usize);
296                        let k_n = SourceLocation::from(ret);
297                        (k_n, v)
298                    })
299                    .collect::<HashMap<SourceLocation, Vec<TracerData>>>();
300
301                let mut tracemap = TraceMap::new();
302                for (k, val) in temp_map.iter().filter(|(k, _)| k.line != 0) {
303                    let rpath = config.strip_base_dir(&k.path);
304                    let mut address = HashSet::new();
305                    let mut fn_name = None;
306                    for v in val.iter() {
307                        if let Some(a) = v.address {
308                            if a < base_addr.address()
309                                && a >= (base_addr.address() + base_addr.size())
310                            {
311                                continue;
312                            }
313                            address.insert(a);
314                            trace!(
315                                "Adding trace at address 0x{:x} in {}:{}",
316                                a,
317                                rpath.display(),
318                                k.line
319                            );
320                        }
321                        if fn_name.is_none() && v.fn_name.is_some() {
322                            fn_name = v.fn_name.clone();
323                        }
324                    }
325                    if address.is_empty() {
326                        trace!(
327                            "Adding trace with no address at {}:{}",
328                            rpath.display(),
329                            k.line
330                        );
331                    }
332                    tracemap.add_trace(&k.path, Trace::new(k.line, address, 1));
333                }
334                result.merge(&tracemap);
335            }
336        }
337    }
338
339    add_line_analysis(analysis, config, &mut result);
340    Ok(result)
341}
342
343fn add_line_analysis(
344    in_analysis: &HashMap<PathBuf, LineAnalysis>,
345    in_config: &Config,
346    in_out_trace: &mut TraceMap,
347) {
348    for (file, line_analysis) in in_analysis.iter() {
349        if in_config.exclude_path(file) || !in_config.include_path(file) {
350            continue;
351        }
352        for line in &line_analysis.cover {
353            let line = *line as u64;
354            if !in_out_trace.contains_location(file, line)
355                && !line_analysis.should_ignore(line as usize)
356            {
357                let rpath = in_config.strip_base_dir(file);
358                trace!(
359                    "Adding trace for potentially uncoverable line in {}:{}",
360                    rpath.display(),
361                    line
362                );
363                in_out_trace.add_trace(file, Trace::new_stub(line));
364            }
365        }
366    }
367}
368
369#[cfg(ptrace_supported)]
370fn open_symbols_file(test: &Path) -> io::Result<File> {
371    File::open(test)
372}
373
374/*
375#[cfg(target_os = "macos")]
376fn open_symbols_file(test: &Path) -> io::Result<File> {
377    let d_sym = test.with_extension("dSYM");
378    File::open(&d_sym)
379}
380*/
381
382#[cfg(not(ptrace_supported))]
383fn open_symbols_file(_test: &Path) -> io::Result<File> {
384    Err(io::Error::new(
385        io::ErrorKind::Other,
386        "Symbol files aren't read on non-ptrace systems",
387    ))
388}
389
390pub fn generate_tracemap(
391    test: &Path,
392    analysis: &HashMap<PathBuf, LineAnalysis>,
393    config: &Config,
394) -> io::Result<TraceMap> {
395    trace!("Generating traces for {}", test.display());
396    let file = match open_symbols_file(test) {
397        Ok(s) => object::read::ReadCache::new(s),
398        Err(e) if config.engine() != TraceEngine::Llvm => return Err(e),
399        _ => {
400            return Ok(TraceMap::new());
401        }
402    };
403    let obj = object::File::parse(&file)
404        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Unable to parse binary"))?;
405    let endian = if obj.is_little_endian() {
406        RunTimeEndian::Little
407    } else {
408        RunTimeEndian::Big
409    };
410    get_line_addresses(endian, &obj, analysis, config)
411        .map_err(|e| {
412            // They may be running with a stripped binary or doing something weird
413            error!("Error parsing debug information from binary: {}", e);
414            warn!("Stripping symbol information can prevent tarpaulin from working. If you want to do this pass `--engine=llvm`");
415            io::Error::new(io::ErrorKind::InvalidData, "Error while parsing binary or DWARF info.")
416        })
417}