cargo_tarpaulin/
cargo.rs

1use crate::config::*;
2use crate::errors::RunError;
3use crate::path_utils::{fix_unc_path, get_source_walker};
4use cargo_metadata::{diagnostic::DiagnosticLevel, CargoOpt, Message, Metadata, MetadataCommand};
5use lazy_static::lazy_static;
6use regex::Regex;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::env;
10use std::ffi::OsStr;
11use std::fs::{read_dir, read_to_string, remove_dir_all, remove_file, File};
12use std::io;
13use std::io::{BufRead, BufReader};
14use std::path::{Component, Path, PathBuf};
15use std::process::{Command, Stdio};
16use toml::Value;
17use tracing::{debug, error, info, trace, warn};
18use walkdir::{DirEntry, WalkDir};
19
20const BUILD_PROFRAW: &str = "build_rs_cov.profraw";
21
22cfg_if::cfg_if! {
23    if #[cfg(target_os = "windows")] {
24        pub const LD_PATH_VAR: &'static str ="PATH";
25    } else if #[cfg(any(target_os = "macos", target_os = "ios"))] {
26        pub const LD_PATH_VAR: &'static str = "DYLD_LIBRARY_PATH";
27    } else {
28        pub const LD_PATH_VAR: &'static str =  "LD_LIBRARY_PATH";
29    }
30}
31
32#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
33enum Channel {
34    Stable,
35    Beta,
36    Nightly,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
40struct CargoVersionInfo {
41    major: usize,
42    minor: usize,
43    channel: Channel,
44}
45
46impl CargoVersionInfo {
47    fn supports_llvm_cov(&self) -> bool {
48        (self.minor >= 50 && self.channel == Channel::Nightly) || self.minor >= 60
49    }
50}
51
52#[derive(Clone, Debug, Default)]
53pub struct CargoOutput {
54    /// This contains all binaries we want to run to collect coverage from.
55    pub test_binaries: Vec<TestBinary>,
56    /// This covers binaries we don't want to run explicitly but may be called as part of tracing
57    /// execution of other processes.
58    pub binaries: Vec<PathBuf>,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
62pub struct TestBinary {
63    path: PathBuf,
64    ty: Option<RunType>,
65    cargo_dir: Option<PathBuf>,
66    pkg_name: Option<String>,
67    pkg_version: Option<String>,
68    pkg_authors: Option<Vec<String>>,
69    should_panic: bool,
70    /// Linker paths used when linking the binary, this should be accessed via
71    /// `Self::has_linker_paths` and `Self::ld_library_path` as there may be interaction with
72    /// current environment. It's only made pub(crate) for the purpose of testing.
73    pub(crate) linker_paths: Vec<PathBuf>,
74}
75
76#[derive(Clone, Debug)]
77struct DocTestBinaryMeta {
78    prefix: String,
79    line: usize,
80}
81
82impl TestBinary {
83    pub fn new(path: PathBuf, ty: Option<RunType>) -> Self {
84        Self {
85            path,
86            ty,
87            pkg_name: None,
88            pkg_version: None,
89            pkg_authors: None,
90            cargo_dir: None,
91            should_panic: false,
92            linker_paths: vec![],
93        }
94    }
95
96    pub fn path(&self) -> &Path {
97        &self.path
98    }
99
100    pub fn run_type(&self) -> Option<RunType> {
101        self.ty
102    }
103
104    pub fn manifest_dir(&self) -> &Option<PathBuf> {
105        &self.cargo_dir
106    }
107
108    pub fn pkg_name(&self) -> &Option<String> {
109        &self.pkg_name
110    }
111
112    pub fn pkg_version(&self) -> &Option<String> {
113        &self.pkg_version
114    }
115
116    pub fn pkg_authors(&self) -> &Option<Vec<String>> {
117        &self.pkg_authors
118    }
119
120    pub fn has_linker_paths(&self) -> bool {
121        !self.linker_paths.is_empty()
122    }
123
124    pub fn is_test_type(&self) -> bool {
125        matches!(self.ty, None | Some(RunType::Tests))
126    }
127
128    /// Convert linker paths to an LD_LIBRARY_PATH.
129    /// TODO this won't work for windows when it's implemented
130    pub fn ld_library_path(&self) -> String {
131        cfg_if::cfg_if! {
132            if #[cfg(windows)] {
133                const PATH_SEP: &str = ";";
134            } else {
135                const PATH_SEP: &str  = ":";
136            }
137        }
138
139        let mut new_vals = self
140            .linker_paths
141            .iter()
142            .map(|x| x.display().to_string())
143            .collect::<Vec<String>>()
144            .join(PATH_SEP);
145        if let Ok(ld) = env::var(LD_PATH_VAR) {
146            new_vals.push_str(PATH_SEP);
147            new_vals.push_str(ld.as_str());
148        }
149        new_vals
150    }
151
152    /// Should be `false` for normal tests and for doctests either `true` or
153    /// `false` depending on the test attribute
154    pub fn should_panic(&self) -> bool {
155        self.should_panic
156    }
157
158    /// Convenience function to get the file name of the binary as a string, default string if the
159    /// path has no filename as this should _never_ happen
160    pub fn file_name(&self) -> String {
161        self.path
162            .file_name()
163            .map(|x| x.to_string_lossy().to_string())
164            .unwrap_or_default()
165    }
166}
167
168impl DocTestBinaryMeta {
169    fn new<P: AsRef<Path>>(test: P) -> Option<Self> {
170        if let Some(Component::Normal(folder)) = test.as_ref().components().nth_back(1) {
171            let temp = folder.to_string_lossy();
172            let file_end = temp.rfind("rs").map(|i| i + 2)?;
173            let end = temp.rfind('_')?;
174            if end > file_end + 1 {
175                let line = temp[(file_end + 1)..end].parse::<usize>().ok()?;
176                Some(Self {
177                    prefix: temp[..file_end].to_string(),
178                    line,
179                })
180            } else {
181                None
182            }
183        } else {
184            None
185        }
186    }
187}
188
189lazy_static! {
190    static ref CARGO_VERSION_INFO: Option<CargoVersionInfo> = {
191        let version_info = Regex::new(
192            r"cargo (\d)\.(\d+)\.\d+([\-betanightly]*)(\.[[:alnum:]]+)?",
193        )
194        .unwrap();
195        Command::new("cargo")
196            .arg("--version")
197            .output()
198            .map(|x| {
199                let s = String::from_utf8_lossy(&x.stdout);
200                if let Some(cap) = version_info.captures(&s) {
201                    let major = cap[1].parse().unwrap();
202                    let minor = cap[2].parse().unwrap();
203                    // We expect a string like `cargo 1.50.0-nightly (a0f433460 2020-02-01)
204                    // the version number either has `-nightly` `-beta` or empty for stable
205                    let channel = match &cap[3] {
206                        "-nightly" => Channel::Nightly,
207                        "-beta" => Channel::Beta,
208                        _ => Channel::Stable,
209                    };
210                    Some(CargoVersionInfo {
211                        major,
212                        minor,
213                        channel,
214                    })
215                } else {
216                    None
217                }
218            })
219            .unwrap_or(None)
220    };
221}
222
223pub fn get_tests(config: &Config) -> Result<CargoOutput, RunError> {
224    let mut result = CargoOutput::default();
225    if config.force_clean() {
226        let cleanup_dir = if config.release {
227            config.target_dir().join("release")
228        } else {
229            config.target_dir().join("debug")
230        };
231        info!("Cleaning project");
232        if cleanup_dir.exists() {
233            if let Err(e) = remove_dir_all(cleanup_dir) {
234                error!("Cargo clean failed: {e}");
235            }
236        }
237    }
238    let man_binding = config.manifest();
239    let manifest = man_binding.as_path().to_str().unwrap_or("Cargo.toml");
240    let metadata = MetadataCommand::new()
241        .manifest_path(manifest)
242        .features(CargoOpt::AllFeatures)
243        .exec()
244        .map_err(|e| RunError::Cargo(e.to_string()))?;
245
246    for ty in &config.run_types {
247        run_cargo(&metadata, manifest, config, Some(*ty), &mut result)?;
248    }
249    if config.has_named_tests() {
250        run_cargo(&metadata, manifest, config, None, &mut result)?;
251    } else if config.run_types.is_empty() {
252        let ty = if config.command == Mode::Test {
253            Some(RunType::Tests)
254        } else {
255            None
256        };
257        run_cargo(&metadata, manifest, config, ty, &mut result)?;
258    }
259    // Only matters for llvm cov and who knows, one day may not be needed
260    let _ = remove_file(config.root().join(BUILD_PROFRAW));
261    Ok(result)
262}
263
264fn run_cargo(
265    metadata: &Metadata,
266    manifest: &str,
267    config: &Config,
268    ty: Option<RunType>,
269    result: &mut CargoOutput,
270) -> Result<(), RunError> {
271    let mut cmd = create_command(manifest, config, ty);
272    if ty != Some(RunType::Doctests) {
273        cmd.stdout(Stdio::piped());
274    } else {
275        clean_doctest_folder(config.doctest_dir());
276        cmd.stdout(Stdio::null());
277    }
278    trace!("Running command {:?}", cmd);
279    let mut child = cmd.spawn().map_err(|e| RunError::Cargo(e.to_string()))?;
280    let update_from = result.test_binaries.len();
281    let mut paths = match get_libdir(ty) {
282        Some(path) => vec![path],
283        None => vec![],
284    };
285
286    if ty != Some(RunType::Doctests) {
287        let mut package_ids = vec![None; result.test_binaries.len()];
288        let reader = std::io::BufReader::new(child.stdout.take().unwrap());
289        let mut error = None;
290        for msg in Message::parse_stream(reader) {
291            match msg {
292                Ok(Message::CompilerArtifact(art)) => {
293                    if let Some(path) = art.executable.as_ref() {
294                        if !art.profile.test && config.command == Mode::Test {
295                            result.binaries.push(PathBuf::from(path));
296                            continue;
297                        }
298                        result
299                            .test_binaries
300                            .push(TestBinary::new(fix_unc_path(path.as_std_path()), ty));
301                        package_ids.push(Some(art.package_id.clone()));
302                    }
303                }
304                Ok(Message::CompilerMessage(m)) => match m.message.level {
305                    DiagnosticLevel::Error | DiagnosticLevel::Ice => {
306                        let msg = if let Some(rendered) = m.message.rendered {
307                            rendered
308                        } else {
309                            format!("{}: {}", m.target.name, m.message.message)
310                        };
311                        error = Some(RunError::TestCompile(msg));
312                        break;
313                    }
314                    _ => {}
315                },
316                Ok(Message::BuildScriptExecuted(bs))
317                    if !(bs.linked_libs.is_empty() && bs.linked_paths.is_empty()) =>
318                {
319                    let temp_paths = bs.linked_paths.iter().filter_map(|x| {
320                        if x.as_std_path().exists() {
321                            Some(x.as_std_path().to_path_buf())
322                        } else if let Some(index) = x.as_str().find('=') {
323                            Some(PathBuf::from(&x.as_str()[(index + 1)..]))
324                        } else {
325                            warn!("Couldn't resolve linker path: {}", x.as_str());
326                            None
327                        }
328                    });
329                    for p in temp_paths {
330                        if !paths.contains(&p) {
331                            paths.push(p);
332                        }
333                    }
334                }
335                Err(e) => {
336                    error!("Error parsing cargo messages {e}");
337                }
338                _ => {}
339            }
340        }
341        debug!("Linker paths: {:?}", paths);
342        for bin in result.test_binaries.iter_mut().skip(update_from) {
343            bin.linker_paths = paths.clone();
344        }
345        let status = child.wait().unwrap();
346        if let Some(error) = error {
347            return Err(error);
348        }
349        if !status.success() {
350            return Err(RunError::Cargo("cargo run failed".to_string()));
351        };
352        for (res, package) in result
353            .test_binaries
354            .iter_mut()
355            .zip(package_ids.iter())
356            .filter(|(_, b)| b.is_some())
357        {
358            if let Some(package) = package {
359                let package = &metadata[package];
360                res.cargo_dir = package
361                    .manifest_path
362                    .parent()
363                    .map(|x| fix_unc_path(x.as_std_path()));
364                res.pkg_name = Some(package.name.clone());
365                res.pkg_version = Some(package.version.to_string());
366                res.pkg_authors = Some(package.authors.clone());
367            }
368        }
369        child.wait().map_err(|e| RunError::Cargo(e.to_string()))?;
370    } else {
371        // need to wait for compiling to finish before getting doctests
372        // also need to wait with output to ensure the stdout buffer doesn't fill up
373        let out = child
374            .wait_with_output()
375            .map_err(|e| RunError::Cargo(e.to_string()))?;
376        if !out.status.success() {
377            error!("Building doctests failed");
378            return Err(RunError::Cargo("Building doctest failed".to_string()));
379        }
380        let walker = WalkDir::new(config.doctest_dir()).into_iter();
381        let dir_entries = walker
382            .filter_map(Result::ok)
383            .filter(|e| matches!(e.metadata(), Ok(ref m) if m.is_file() && m.len() != 0))
384            .filter(|e| {
385                let ext = e.path().extension();
386                ext != Some(OsStr::new("pdb"))
387                    && ext != Some(OsStr::new("rs"))
388                    && ext != Some(OsStr::new("rlib"))
389            })
390            .filter(|e| {
391                !e.path()
392                    .components()
393                    .any(|x| x.as_os_str().to_string_lossy().contains("dSYM"))
394            })
395            .collect::<Vec<_>>();
396
397        let should_panics = get_attribute_candidates(&dir_entries, config, "should_panic");
398        let no_runs = get_attribute_candidates(&dir_entries, config, "no_run");
399        for dt in &dir_entries {
400            let mut tb = TestBinary::new(fix_unc_path(dt.path()), ty);
401
402            if let Some(meta) = DocTestBinaryMeta::new(dt.path()) {
403                if no_runs
404                    .get(&meta.prefix)
405                    .map(|x| x.contains(&meta.line))
406                    .unwrap_or(false)
407                {
408                    info!("Skipping no_run doctest: {}", dt.path().display());
409                    continue;
410                }
411                if let Some(lines) = should_panics.get(&meta.prefix) {
412                    tb.should_panic |= lines.contains(&meta.line);
413                }
414            }
415            let mut current_dir = dt.path();
416            loop {
417                if current_dir.is_dir() && current_dir.join("Cargo.toml").exists() {
418                    tb.cargo_dir = Some(fix_unc_path(current_dir));
419                    break;
420                }
421                match current_dir.parent() {
422                    Some(s) => {
423                        current_dir = s;
424                    }
425                    None => break,
426                }
427            }
428            result.test_binaries.push(tb);
429        }
430    }
431    Ok(())
432}
433
434fn convert_to_prefix(p: &Path) -> Option<String> {
435    let mut buffer = vec![];
436    let mut p = Some(p);
437    while let Some(path_temp) = p {
438        // The only component of the path that should be lacking a filename is the final empty
439        // parent of a relative path, which we don't want to include anyway.
440        if let Some(name) = path_temp.file_name().and_then(|s| s.to_str()) {
441            buffer.push(name.replace(['.', '-'], "_"));
442        }
443        p = path_temp.parent();
444    }
445    if buffer.is_empty() {
446        None
447    } else {
448        buffer.reverse();
449        Some(buffer.join("_"))
450    }
451}
452
453fn is_prefix_match(prefix: &str, entry: &Path) -> bool {
454    convert_to_prefix(entry).as_deref() == Some(prefix)
455}
456
457/// This returns a map of the string prefixes for the file in the doc test and a list of lines
458/// which contain the string `should_panic` it makes no guarantees that all these lines are a
459/// doctest attribute showing panic behaviour (but some of them will be)
460///
461/// Currently all doctest files take the pattern of `{name}_{line}_{number}` where name is the
462/// path to the file with directory separators and dots replaced with underscores. Therefore
463/// each name could potentially map to many files as `src_some_folder_foo_rs_1_1` could go to
464/// `src/some/folder_foo.rs` or `src/some/folder/foo.rs` here we're going to work on a heuristic
465/// that any matching file is good because we can't do any better
466///
467/// As of some point in June 2023 the naming convention has changed to include the package name in
468/// the generated name which reduces collisions. Before it was done relative to the workspace
469/// package folder not the workspace root.
470fn get_attribute_candidates(
471    tests: &[DirEntry],
472    config: &Config,
473    attribute: &str,
474) -> HashMap<String, Vec<usize>> {
475    let mut result = HashMap::new();
476    let mut checked_files = HashSet::new();
477    let root = config.root();
478    for test in tests {
479        if let Some(test_binary) = DocTestBinaryMeta::new(test.path()) {
480            for dir_entry in get_source_walker(config) {
481                let path = dir_entry.path();
482                if path.is_file() {
483                    if let Some(p) = path_relative_from(path, &root) {
484                        if is_prefix_match(&test_binary.prefix, &p) && !checked_files.contains(path)
485                        {
486                            checked_files.insert(path.to_path_buf());
487                            let lines = find_str_in_file(path, attribute).unwrap_or_default();
488                            if !result.contains_key(&test_binary.prefix) {
489                                result.insert(test_binary.prefix.clone(), lines);
490                            } else if let Some(current_lines) = result.get_mut(&test_binary.prefix)
491                            {
492                                current_lines.extend_from_slice(&lines);
493                            }
494                        }
495                    }
496                }
497            }
498        } else {
499            warn!(
500                "Invalid characters in name of doctest {}",
501                test.path().display()
502            );
503        }
504    }
505    result
506}
507
508fn find_str_in_file(file: &Path, value: &str) -> io::Result<Vec<usize>> {
509    let f = File::open(file)?;
510    let reader = BufReader::new(f);
511    let lines = reader
512        .lines()
513        .enumerate()
514        .filter(|(_, l)| l.as_ref().map(|x| x.contains(value)).unwrap_or(false))
515        .map(|(i, _)| i + 1) // Move from line index to line number
516        .collect();
517    Ok(lines)
518}
519
520fn start_cargo_command(ty: Option<RunType>) -> Command {
521    let mut test_cmd = Command::new("cargo");
522    let bootstrap = matches!(env::var("RUSTC_BOOTSTRAP").as_deref(), Ok("1"));
523    let override_toolchain = if cfg!(windows) {
524        let rustup_home = env::var("RUSTUP_HOME").unwrap_or(".rustup".into());
525        if env::var("PATH").unwrap_or_default().contains(&rustup_home) {
526            // So the specific cargo we're using is in the path var so rustup toolchains won't
527            // work. This only started happening recently so special casing it for older versions
528            env::remove_var("RUSTUP_TOOLCHAIN");
529            false
530        } else {
531            true
532        }
533    } else {
534        true
535    };
536    if ty == Some(RunType::Doctests) {
537        if override_toolchain {
538            if let Some(toolchain) = env::var("RUSTUP_TOOLCHAIN")
539                .ok()
540                .filter(|t| t.starts_with("nightly") || bootstrap)
541            {
542                test_cmd.args([format!("+{toolchain}").as_str()]);
543            } else if !bootstrap && !is_nightly() {
544                test_cmd.args(["+nightly"]);
545            }
546        }
547    } else {
548        if override_toolchain {
549            if let Ok(toolchain) = env::var("RUSTUP_TOOLCHAIN") {
550                test_cmd.arg(format!("+{toolchain}"));
551            }
552        }
553    }
554    test_cmd
555}
556
557fn get_libdir(ty: Option<RunType>) -> Option<PathBuf> {
558    let mut test_cmd = start_cargo_command(ty);
559    test_cmd.env("RUSTC_BOOTSTRAP", "1");
560    test_cmd.args(["rustc", "-Z", "unstable-options", "--print=target-libdir"]);
561
562    let output = match test_cmd.output() {
563        Ok(output) => String::from_utf8_lossy(&output.stdout).trim().to_string(),
564        Err(e) => {
565            debug!("Unable to run cargo rustc command: {}", e);
566            warn!("Unable to get target libdir proc macro crates in the workspace may not work. Consider adding `--exclude` to remove them from compilation");
567            return None;
568        }
569    };
570    Some(PathBuf::from(output))
571}
572
573fn create_command(manifest_path: &str, config: &Config, ty: Option<RunType>) -> Command {
574    let mut test_cmd = start_cargo_command(ty);
575    if ty == Some(RunType::Doctests) {
576        test_cmd.args(["test"]);
577    } else {
578        if config.command == Mode::Test {
579            test_cmd.args(["test", "--no-run"]);
580        } else {
581            test_cmd.arg("build");
582        }
583    }
584    test_cmd.args(["--message-format", "json", "--manifest-path", manifest_path]);
585    if let Some(ty) = ty {
586        match ty {
587            RunType::Tests => test_cmd.arg("--tests"),
588            RunType::Doctests => test_cmd.arg("--doc"),
589            RunType::Benchmarks => test_cmd.arg("--benches"),
590            RunType::Examples => test_cmd.arg("--examples"),
591            RunType::AllTargets => test_cmd.arg("--all-targets"),
592            RunType::Lib => test_cmd.arg("--lib"),
593            RunType::Bins => test_cmd.arg("--bins"),
594        };
595    } else {
596        for test in &config.test_names {
597            test_cmd.arg("--test");
598            test_cmd.arg(test);
599        }
600        for test in &config.bin_names {
601            test_cmd.arg("--bin");
602            test_cmd.arg(test);
603        }
604        for test in &config.example_names {
605            test_cmd.arg("--example");
606            test_cmd.arg(test);
607        }
608        for test in &config.bench_names {
609            test_cmd.arg("--bench");
610            test_cmd.arg(test);
611        }
612    }
613    init_args(&mut test_cmd, config);
614    setup_environment(&mut test_cmd, config);
615    test_cmd
616}
617
618fn init_args(test_cmd: &mut Command, config: &Config) {
619    if config.debug {
620        test_cmd.arg("-vvv");
621    } else if config.verbose {
622        test_cmd.arg("-v");
623    }
624    if config.locked {
625        test_cmd.arg("--locked");
626    }
627    if config.frozen {
628        test_cmd.arg("--frozen");
629    }
630    if config.no_fail_fast {
631        test_cmd.arg("--no-fail-fast");
632    }
633    if let Some(profile) = config.profile.as_ref() {
634        test_cmd.arg("--profile");
635        test_cmd.arg(profile);
636    }
637    if let Some(jobs) = config.jobs {
638        test_cmd.arg("--jobs");
639        test_cmd.arg(jobs.to_string());
640    }
641    if let Some(features) = config.features.as_ref() {
642        test_cmd.arg("--features");
643        test_cmd.arg(features);
644    }
645    if config.all_features {
646        test_cmd.arg("--all-features");
647    }
648    if config.no_default_features {
649        test_cmd.arg("--no-default-features");
650    }
651    if config.all {
652        test_cmd.arg("--workspace");
653    }
654    if config.release {
655        test_cmd.arg("--release");
656    }
657    config.packages.iter().for_each(|package| {
658        test_cmd.arg("--package");
659        test_cmd.arg(package);
660    });
661    config.exclude.iter().for_each(|package| {
662        test_cmd.arg("--exclude");
663        test_cmd.arg(package);
664    });
665    test_cmd.arg("--color");
666    test_cmd.arg(config.color.to_string().to_ascii_lowercase());
667    if let Some(target) = config.target.as_ref() {
668        test_cmd.args(["--target", target]);
669    }
670    let args = vec![
671        "--target-dir".to_string(),
672        format!("{}", config.target_dir().display()),
673    ];
674    test_cmd.args(args);
675    if config.offline {
676        test_cmd.arg("--offline");
677    }
678    for feat in &config.unstable_features {
679        test_cmd.arg(format!("-Z{feat}"));
680    }
681    if config.command == Mode::Test && !config.varargs.is_empty() {
682        let mut args = vec!["--".to_string()];
683        args.extend_from_slice(&config.varargs);
684        test_cmd.args(args);
685    }
686}
687
688/// Old doc tests that no longer exist or where the line have changed can persist so delete them to
689/// avoid confusing the results
690fn clean_doctest_folder<P: AsRef<Path>>(doctest_dir: P) {
691    if let Ok(rd) = read_dir(doctest_dir.as_ref()) {
692        rd.flat_map(Result::ok)
693            .filter(|e| {
694                e.path()
695                    .components()
696                    .next_back()
697                    .map(|e| e.as_os_str().to_string_lossy().contains("rs"))
698                    .unwrap_or(false)
699            })
700            .for_each(|e| {
701                if let Err(err) = remove_dir_all(e.path()) {
702                    warn!("Failed to delete {}: {}", e.path().display(), err);
703                }
704            });
705    }
706}
707
708fn handle_llvm_flags(value: &mut String, config: &Config) {
709    if config.engine() == TraceEngine::Llvm {
710        value.push_str(llvm_coverage_rustflag());
711    } else if !config.no_dead_code {
712        value.push_str(" -Clink-dead-code ");
713    }
714}
715
716fn look_for_field_in_table(value: &Value, field: &str) -> String {
717    let table = value.as_table().unwrap();
718
719    if let Some(rustflags) = table.get(field) {
720        if rustflags.is_array() {
721            let vec_of_flags: Vec<String> = rustflags
722                .as_array()
723                .unwrap()
724                .iter()
725                .filter_map(Value::as_str)
726                .map(ToString::to_string)
727                .collect();
728
729            vec_of_flags.join(" ")
730        } else if rustflags.is_str() {
731            rustflags.as_str().unwrap().to_string()
732        } else {
733            String::new()
734        }
735    } else {
736        String::new()
737    }
738}
739
740fn look_for_field_in_file(path: &Path, section: &str, field: &str) -> Option<String> {
741    if let Ok(contents) = read_to_string(path) {
742        let value = contents.parse::<Value>().ok()?;
743
744        let value: Vec<String> = value
745            .as_table()?
746            .into_iter()
747            .map(|(s, v)| {
748                if s.as_str() == section {
749                    look_for_field_in_table(v, field)
750                } else {
751                    String::new()
752                }
753            })
754            .collect();
755
756        Some(value.join(" "))
757    } else {
758        None
759    }
760}
761
762fn look_for_field_in_section(path: &Path, section: &str, field: &str) -> Option<String> {
763    let mut config_path = path.join("config");
764
765    let value = look_for_field_in_file(&config_path, section, field);
766    if value.is_some() {
767        return value;
768    }
769
770    config_path.pop();
771    config_path.push("config.toml");
772
773    let value = look_for_field_in_file(&config_path, section, field);
774    if value.is_some() {
775        return value;
776    }
777
778    None
779}
780
781fn build_config_path(base: impl AsRef<Path>) -> PathBuf {
782    let mut config_path = PathBuf::from(base.as_ref());
783    config_path.push(base);
784    config_path.push(".cargo");
785
786    config_path
787}
788
789fn gather_config_field_from_section(config: &Config, section: &str, field: &str) -> String {
790    if let Some(value) =
791        look_for_field_in_section(&build_config_path(config.root()), section, field)
792    {
793        return value;
794    }
795
796    if let Ok(cargo_home_config) = env::var("CARGO_HOME") {
797        if let Some(value) =
798            look_for_field_in_section(&PathBuf::from(cargo_home_config), section, field)
799        {
800            return value;
801        }
802    }
803
804    String::new()
805}
806
807pub fn rust_flags(config: &Config) -> String {
808    const RUSTFLAGS: &str = "RUSTFLAGS";
809    let mut value = config.rustflags.clone().unwrap_or_default();
810    value.push_str(" -Cdebuginfo=2 ");
811    value.push_str("-Cstrip=none ");
812    if !config.avoid_cfg_tarpaulin {
813        value.push_str("--cfg=tarpaulin ");
814    }
815    if config.release {
816        value.push_str("-Cdebug-assertions=off ");
817    }
818    handle_llvm_flags(&mut value, config);
819    lazy_static! {
820        static ref DEBUG_INFO: Regex = Regex::new(r"\-C\s*debuginfo=\d").unwrap();
821        static ref DEAD_CODE: Regex = Regex::new(r"\-C\s*link-dead-code").unwrap();
822    }
823    if let Ok(vtemp) = env::var(RUSTFLAGS) {
824        let temp = DEBUG_INFO.replace_all(&vtemp, " ");
825        if config.no_dead_code {
826            value.push_str(&DEAD_CODE.replace_all(&temp, " "));
827        } else {
828            value.push_str(&temp);
829        }
830    } else {
831        let vtemp = gather_config_field_from_section(config, "build", "rustflags");
832        value.push_str(&DEBUG_INFO.replace_all(&vtemp, " "));
833    }
834
835    deduplicate_flags(&value)
836}
837
838pub fn rustdoc_flags(config: &Config) -> String {
839    const RUSTDOC: &str = "RUSTDOCFLAGS";
840    let common_opts = " -Cdebuginfo=2 --cfg=tarpaulin -Cstrip=none ";
841    let mut value = format!(
842        "{} --persist-doctests {} -Zunstable-options ",
843        common_opts,
844        config.doctest_dir().display()
845    );
846    if let Ok(vtemp) = env::var(RUSTDOC) {
847        if !vtemp.contains("--persist-doctests") {
848            value.push_str(vtemp.as_ref());
849        }
850    } else {
851        let vtemp = gather_config_field_from_section(config, "build", "rustdocflags");
852        value.push_str(&vtemp);
853    }
854    handle_llvm_flags(&mut value, config);
855    deduplicate_flags(&value)
856}
857
858fn deduplicate_flags(flags: &str) -> String {
859    lazy_static! {
860        static ref CFG_FLAG: Regex = Regex::new(r#"\--cfg\s+"#).unwrap();
861        static ref C_FLAG: Regex = Regex::new(r#"\-C\s+"#).unwrap();
862        static ref Z_FLAG: Regex = Regex::new(r#"\-Z\s+"#).unwrap();
863        static ref W_FLAG: Regex = Regex::new(r#"\-W\s+"#).unwrap();
864        static ref A_FLAG: Regex = Regex::new(r#"\-A\s+"#).unwrap();
865        static ref D_FLAG: Regex = Regex::new(r#"\-D\s+"#).unwrap();
866    }
867
868    // Going to remove the excess spaces to make it easier to filter things.
869    let res = CFG_FLAG.replace_all(flags, "--cfg=");
870    let res = C_FLAG.replace_all(&res, "-C");
871    let res = Z_FLAG.replace_all(&res, "-Z");
872    let res = W_FLAG.replace_all(&res, "-W");
873    let res = A_FLAG.replace_all(&res, "-A");
874    let res = D_FLAG.replace_all(&res, "-D");
875
876    let mut flag_set = HashSet::new();
877    let mut result = vec![];
878    for val in res.split_whitespace() {
879        if val.starts_with("--cfg") {
880            if !flag_set.contains(&val) {
881                result.push(val);
882                flag_set.insert(val);
883            }
884        } else {
885            let id = val.split('=').next().unwrap();
886            if !flag_set.contains(id) {
887                flag_set.insert(id);
888                result.push(val);
889            }
890        }
891    }
892    result.join(" ")
893}
894
895fn setup_environment(cmd: &mut Command, config: &Config) {
896    // https://github.com/rust-lang/rust/issues/107447
897    cmd.env("LLVM_PROFILE_FILE", config.root().join(BUILD_PROFRAW));
898    cmd.env("TARPAULIN", "1");
899    let rustflags = "RUSTFLAGS";
900    let value = rust_flags(config);
901    cmd.env(rustflags, value);
902    // doesn't matter if we don't use it
903    let rustdoc = "RUSTDOCFLAGS";
904    let value = rustdoc_flags(config);
905    trace!("Setting RUSTDOCFLAGS='{}'", value);
906    cmd.env(rustdoc, value);
907    if let Ok(bootstrap) = env::var("RUSTC_BOOTSTRAP") {
908        cmd.env("RUSTC_BOOTSTRAP", bootstrap);
909    }
910}
911
912/// Taking the output of cargo version command return true if it's known to be a nightly channel
913/// false otherwise.
914fn is_nightly() -> bool {
915    if let Some(version) = CARGO_VERSION_INFO.as_ref() {
916        version.channel == Channel::Nightly
917    } else {
918        false
919    }
920}
921
922pub fn supports_llvm_coverage() -> bool {
923    if let Some(version) = CARGO_VERSION_INFO.as_ref() {
924        version.supports_llvm_cov()
925    } else {
926        false
927    }
928}
929
930pub fn llvm_coverage_rustflag() -> &'static str {
931    match CARGO_VERSION_INFO.as_ref() {
932        Some(v) if v.minor >= 60 => " -Cinstrument-coverage ",
933        _ => " -Zinstrument-coverage ",
934    }
935}
936
937#[cfg(test)]
938mod tests {
939    use super::*;
940    use toml::toml;
941
942    #[test]
943    fn can_get_libdir() {
944        let path = get_libdir(Some(RunType::Tests)).unwrap();
945        assert!(path.exists(), "{} doesn't exist", path.display());
946    }
947
948    #[test]
949    #[cfg(not(any(windows, target_os = "macos")))]
950    fn check_dead_code_flags() {
951        let mut config = Config::default();
952        config.set_engine(TraceEngine::Ptrace);
953        assert!(rustdoc_flags(&config).contains("link-dead-code"));
954        assert!(rust_flags(&config).contains("link-dead-code"));
955
956        config.no_dead_code = true;
957        assert!(!rustdoc_flags(&config).contains("link-dead-code"));
958        assert!(!rust_flags(&config).contains("link-dead-code"));
959    }
960
961    #[test]
962    fn parse_rustflags_from_toml() {
963        let list_flags = toml! {
964            rustflags = ["--cfg=foo", "--cfg=bar"]
965        };
966        let list_flags = toml::Value::Table(list_flags);
967
968        assert_eq!(
969            look_for_field_in_table(&list_flags, "rustflags"),
970            "--cfg=foo --cfg=bar"
971        );
972
973        let string_flags = toml! {
974            rustflags = "--cfg=bar --cfg=baz"
975        };
976        let string_flags = toml::Value::Table(string_flags);
977
978        assert_eq!(
979            look_for_field_in_table(&string_flags, "rustflags"),
980            "--cfg=bar --cfg=baz"
981        );
982    }
983
984    #[test]
985    fn llvm_cov_compatible_version() {
986        let version = CargoVersionInfo {
987            major: 1,
988            minor: 50,
989            channel: Channel::Nightly,
990        };
991        assert!(version.supports_llvm_cov());
992        let version = CargoVersionInfo {
993            major: 1,
994            minor: 60,
995            channel: Channel::Stable,
996        };
997        assert!(version.supports_llvm_cov());
998    }
999
1000    #[test]
1001    fn llvm_cov_incompatible_version() {
1002        let mut version = CargoVersionInfo {
1003            major: 1,
1004            minor: 48,
1005            channel: Channel::Stable,
1006        };
1007        assert!(!version.supports_llvm_cov());
1008        version.channel = Channel::Beta;
1009        assert!(!version.supports_llvm_cov());
1010        version.minor = 50;
1011        assert!(!version.supports_llvm_cov());
1012        version.minor = 58;
1013        version.channel = Channel::Stable;
1014        assert!(!version.supports_llvm_cov());
1015    }
1016
1017    #[test]
1018    fn no_duplicate_flags() {
1019        assert_eq!(
1020            deduplicate_flags("--cfg=tarpaulin --cfg tarpaulin"),
1021            "--cfg=tarpaulin"
1022        );
1023        assert_eq!(
1024            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -C link-dead-code"),
1025            "-Clink-dead-code -Zinstrument-coverage"
1026        );
1027        assert_eq!(
1028            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -Zinstrument-coverage"),
1029            "-Clink-dead-code -Zinstrument-coverage"
1030        );
1031        assert_eq!(
1032            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -Cinstrument-coverage"),
1033            "-Clink-dead-code -Zinstrument-coverage -Cinstrument-coverage"
1034        );
1035
1036        assert_eq!(
1037            deduplicate_flags("--cfg=tarpaulin --cfg tarpauline --cfg=tarp"),
1038            "--cfg=tarpaulin --cfg=tarpauline --cfg=tarp"
1039        );
1040    }
1041}