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")) && ext != Some(OsStr::new("rs"))
387            })
388            .filter(|e| {
389                !e.path()
390                    .components()
391                    .any(|x| x.as_os_str().to_string_lossy().contains("dSYM"))
392            })
393            .collect::<Vec<_>>();
394
395        let should_panics = get_attribute_candidates(&dir_entries, config, "should_panic");
396        let no_runs = get_attribute_candidates(&dir_entries, config, "no_run");
397        for dt in &dir_entries {
398            let mut tb = TestBinary::new(fix_unc_path(dt.path()), ty);
399
400            if let Some(meta) = DocTestBinaryMeta::new(dt.path()) {
401                if no_runs
402                    .get(&meta.prefix)
403                    .map(|x| x.contains(&meta.line))
404                    .unwrap_or(false)
405                {
406                    info!("Skipping no_run doctest: {}", dt.path().display());
407                    continue;
408                }
409                if let Some(lines) = should_panics.get(&meta.prefix) {
410                    tb.should_panic |= lines.contains(&meta.line);
411                }
412            }
413            let mut current_dir = dt.path();
414            loop {
415                if current_dir.is_dir() && current_dir.join("Cargo.toml").exists() {
416                    tb.cargo_dir = Some(fix_unc_path(current_dir));
417                    break;
418                }
419                match current_dir.parent() {
420                    Some(s) => {
421                        current_dir = s;
422                    }
423                    None => break,
424                }
425            }
426            result.test_binaries.push(tb);
427        }
428    }
429    Ok(())
430}
431
432fn convert_to_prefix(p: &Path) -> Option<String> {
433    let mut buffer = vec![];
434    let mut p = Some(p);
435    while let Some(path_temp) = p {
436        // The only component of the path that should be lacking a filename is the final empty
437        // parent of a relative path, which we don't want to include anyway.
438        if let Some(name) = path_temp.file_name().and_then(|s| s.to_str()) {
439            buffer.push(name.replace(['.', '-'], "_"));
440        }
441        p = path_temp.parent();
442    }
443    if buffer.is_empty() {
444        None
445    } else {
446        buffer.reverse();
447        Some(buffer.join("_"))
448    }
449}
450
451fn is_prefix_match(prefix: &str, entry: &Path) -> bool {
452    convert_to_prefix(entry).as_deref() == Some(prefix)
453}
454
455/// This returns a map of the string prefixes for the file in the doc test and a list of lines
456/// which contain the string `should_panic` it makes no guarantees that all these lines are a
457/// doctest attribute showing panic behaviour (but some of them will be)
458///
459/// Currently all doctest files take the pattern of `{name}_{line}_{number}` where name is the
460/// path to the file with directory separators and dots replaced with underscores. Therefore
461/// each name could potentially map to many files as `src_some_folder_foo_rs_1_1` could go to
462/// `src/some/folder_foo.rs` or `src/some/folder/foo.rs` here we're going to work on a heuristic
463/// that any matching file is good because we can't do any better
464///
465/// As of some point in June 2023 the naming convention has changed to include the package name in
466/// the generated name which reduces collisions. Before it was done relative to the workspace
467/// package folder not the workspace root.
468fn get_attribute_candidates(
469    tests: &[DirEntry],
470    config: &Config,
471    attribute: &str,
472) -> HashMap<String, Vec<usize>> {
473    let mut result = HashMap::new();
474    let mut checked_files = HashSet::new();
475    let root = config.root();
476    for test in tests {
477        if let Some(test_binary) = DocTestBinaryMeta::new(test.path()) {
478            for dir_entry in get_source_walker(config) {
479                let path = dir_entry.path();
480                if path.is_file() {
481                    if let Some(p) = path_relative_from(path, &root) {
482                        if is_prefix_match(&test_binary.prefix, &p) && !checked_files.contains(path)
483                        {
484                            checked_files.insert(path.to_path_buf());
485                            let lines = find_str_in_file(path, attribute).unwrap_or_default();
486                            if !result.contains_key(&test_binary.prefix) {
487                                result.insert(test_binary.prefix.clone(), lines);
488                            } else if let Some(current_lines) = result.get_mut(&test_binary.prefix)
489                            {
490                                current_lines.extend_from_slice(&lines);
491                            }
492                        }
493                    }
494                }
495            }
496        } else {
497            warn!(
498                "Invalid characters in name of doctest {}",
499                test.path().display()
500            );
501        }
502    }
503    result
504}
505
506fn find_str_in_file(file: &Path, value: &str) -> io::Result<Vec<usize>> {
507    let f = File::open(file)?;
508    let reader = BufReader::new(f);
509    let lines = reader
510        .lines()
511        .enumerate()
512        .filter(|(_, l)| l.as_ref().map(|x| x.contains(value)).unwrap_or(false))
513        .map(|(i, _)| i + 1) // Move from line index to line number
514        .collect();
515    Ok(lines)
516}
517
518fn start_cargo_command(ty: Option<RunType>) -> Command {
519    let mut test_cmd = Command::new("cargo");
520    let bootstrap = matches!(env::var("RUSTC_BOOTSTRAP").as_deref(), Ok("1"));
521    let override_toolchain = if cfg!(windows) {
522        let rustup_home = env::var("RUSTUP_HOME").unwrap_or(".rustup".into());
523        if env::var("PATH").unwrap_or_default().contains(&rustup_home) {
524            // So the specific cargo we're using is in the path var so rustup toolchains won't
525            // work. This only started happening recently so special casing it for older versions
526            env::remove_var("RUSTUP_TOOLCHAIN");
527            false
528        } else {
529            true
530        }
531    } else {
532        true
533    };
534    if ty == Some(RunType::Doctests) {
535        if override_toolchain {
536            if let Some(toolchain) = env::var("RUSTUP_TOOLCHAIN")
537                .ok()
538                .filter(|t| t.starts_with("nightly") || bootstrap)
539            {
540                test_cmd.args([format!("+{toolchain}").as_str()]);
541            } else if !bootstrap && !is_nightly() {
542                test_cmd.args(["+nightly"]);
543            }
544        }
545    } else {
546        if override_toolchain {
547            if let Ok(toolchain) = env::var("RUSTUP_TOOLCHAIN") {
548                test_cmd.arg(format!("+{toolchain}"));
549            }
550        }
551    }
552    test_cmd
553}
554
555fn get_libdir(ty: Option<RunType>) -> Option<PathBuf> {
556    let mut test_cmd = start_cargo_command(ty);
557    test_cmd.env("RUSTC_BOOTSTRAP", "1");
558    test_cmd.args(["rustc", "-Z", "unstable-options", "--print=target-libdir"]);
559
560    let output = match test_cmd.output() {
561        Ok(output) => String::from_utf8_lossy(&output.stdout).trim().to_string(),
562        Err(e) => {
563            debug!("Unable to run cargo rustc command: {}", e);
564            warn!("Unable to get target libdir proc macro crates in the workspace may not work. Consider adding `--exclude` to remove them from compilation");
565            return None;
566        }
567    };
568    Some(PathBuf::from(output))
569}
570
571fn create_command(manifest_path: &str, config: &Config, ty: Option<RunType>) -> Command {
572    let mut test_cmd = start_cargo_command(ty);
573    if ty == Some(RunType::Doctests) {
574        test_cmd.args(["test"]);
575    } else {
576        if config.command == Mode::Test {
577            test_cmd.args(["test", "--no-run"]);
578        } else {
579            test_cmd.arg("build");
580        }
581    }
582    test_cmd.args(["--message-format", "json", "--manifest-path", manifest_path]);
583    if let Some(ty) = ty {
584        match ty {
585            RunType::Tests => test_cmd.arg("--tests"),
586            RunType::Doctests => test_cmd.arg("--doc"),
587            RunType::Benchmarks => test_cmd.arg("--benches"),
588            RunType::Examples => test_cmd.arg("--examples"),
589            RunType::AllTargets => test_cmd.arg("--all-targets"),
590            RunType::Lib => test_cmd.arg("--lib"),
591            RunType::Bins => test_cmd.arg("--bins"),
592        };
593    } else {
594        for test in &config.test_names {
595            test_cmd.arg("--test");
596            test_cmd.arg(test);
597        }
598        for test in &config.bin_names {
599            test_cmd.arg("--bin");
600            test_cmd.arg(test);
601        }
602        for test in &config.example_names {
603            test_cmd.arg("--example");
604            test_cmd.arg(test);
605        }
606        for test in &config.bench_names {
607            test_cmd.arg("--bench");
608            test_cmd.arg(test);
609        }
610    }
611    init_args(&mut test_cmd, config);
612    setup_environment(&mut test_cmd, config);
613    test_cmd
614}
615
616fn init_args(test_cmd: &mut Command, config: &Config) {
617    if config.debug {
618        test_cmd.arg("-vvv");
619    } else if config.verbose {
620        test_cmd.arg("-v");
621    }
622    if config.locked {
623        test_cmd.arg("--locked");
624    }
625    if config.frozen {
626        test_cmd.arg("--frozen");
627    }
628    if config.no_fail_fast {
629        test_cmd.arg("--no-fail-fast");
630    }
631    if let Some(profile) = config.profile.as_ref() {
632        test_cmd.arg("--profile");
633        test_cmd.arg(profile);
634    }
635    if let Some(jobs) = config.jobs {
636        test_cmd.arg("--jobs");
637        test_cmd.arg(jobs.to_string());
638    }
639    if let Some(features) = config.features.as_ref() {
640        test_cmd.arg("--features");
641        test_cmd.arg(features);
642    }
643    if config.all_features {
644        test_cmd.arg("--all-features");
645    }
646    if config.no_default_features {
647        test_cmd.arg("--no-default-features");
648    }
649    if config.all {
650        test_cmd.arg("--workspace");
651    }
652    if config.release {
653        test_cmd.arg("--release");
654    }
655    config.packages.iter().for_each(|package| {
656        test_cmd.arg("--package");
657        test_cmd.arg(package);
658    });
659    config.exclude.iter().for_each(|package| {
660        test_cmd.arg("--exclude");
661        test_cmd.arg(package);
662    });
663    test_cmd.arg("--color");
664    test_cmd.arg(config.color.to_string().to_ascii_lowercase());
665    if let Some(target) = config.target.as_ref() {
666        test_cmd.args(["--target", target]);
667    }
668    let args = vec![
669        "--target-dir".to_string(),
670        format!("{}", config.target_dir().display()),
671    ];
672    test_cmd.args(args);
673    if config.offline {
674        test_cmd.arg("--offline");
675    }
676    for feat in &config.unstable_features {
677        test_cmd.arg(format!("-Z{feat}"));
678    }
679    if config.command == Mode::Test && !config.varargs.is_empty() {
680        let mut args = vec!["--".to_string()];
681        args.extend_from_slice(&config.varargs);
682        test_cmd.args(args);
683    }
684}
685
686/// Old doc tests that no longer exist or where the line have changed can persist so delete them to
687/// avoid confusing the results
688fn clean_doctest_folder<P: AsRef<Path>>(doctest_dir: P) {
689    if let Ok(rd) = read_dir(doctest_dir.as_ref()) {
690        rd.flat_map(Result::ok)
691            .filter(|e| {
692                e.path()
693                    .components()
694                    .next_back()
695                    .map(|e| e.as_os_str().to_string_lossy().contains("rs"))
696                    .unwrap_or(false)
697            })
698            .for_each(|e| {
699                if let Err(err) = remove_dir_all(e.path()) {
700                    warn!("Failed to delete {}: {}", e.path().display(), err);
701                }
702            });
703    }
704}
705
706fn handle_llvm_flags(value: &mut String, config: &Config) {
707    if config.engine() == TraceEngine::Llvm {
708        value.push_str(llvm_coverage_rustflag());
709    } else if !config.no_dead_code {
710        value.push_str(" -Clink-dead-code ");
711    }
712}
713
714fn look_for_field_in_table(value: &Value, field: &str) -> String {
715    let table = value.as_table().unwrap();
716
717    if let Some(rustflags) = table.get(field) {
718        if rustflags.is_array() {
719            let vec_of_flags: Vec<String> = rustflags
720                .as_array()
721                .unwrap()
722                .iter()
723                .filter_map(Value::as_str)
724                .map(ToString::to_string)
725                .collect();
726
727            vec_of_flags.join(" ")
728        } else if rustflags.is_str() {
729            rustflags.as_str().unwrap().to_string()
730        } else {
731            String::new()
732        }
733    } else {
734        String::new()
735    }
736}
737
738fn look_for_field_in_file(path: &Path, section: &str, field: &str) -> Option<String> {
739    if let Ok(contents) = read_to_string(path) {
740        let value = contents.parse::<Value>().ok()?;
741
742        let value: Vec<String> = value
743            .as_table()?
744            .into_iter()
745            .map(|(s, v)| {
746                if s.as_str() == section {
747                    look_for_field_in_table(v, field)
748                } else {
749                    String::new()
750                }
751            })
752            .collect();
753
754        Some(value.join(" "))
755    } else {
756        None
757    }
758}
759
760fn look_for_field_in_section(path: &Path, section: &str, field: &str) -> Option<String> {
761    let mut config_path = path.join("config");
762
763    let value = look_for_field_in_file(&config_path, section, field);
764    if value.is_some() {
765        return value;
766    }
767
768    config_path.pop();
769    config_path.push("config.toml");
770
771    let value = look_for_field_in_file(&config_path, section, field);
772    if value.is_some() {
773        return value;
774    }
775
776    None
777}
778
779fn build_config_path(base: impl AsRef<Path>) -> PathBuf {
780    let mut config_path = PathBuf::from(base.as_ref());
781    config_path.push(base);
782    config_path.push(".cargo");
783
784    config_path
785}
786
787fn gather_config_field_from_section(config: &Config, section: &str, field: &str) -> String {
788    if let Some(value) =
789        look_for_field_in_section(&build_config_path(config.root()), section, field)
790    {
791        return value;
792    }
793
794    if let Ok(cargo_home_config) = env::var("CARGO_HOME") {
795        if let Some(value) =
796            look_for_field_in_section(&PathBuf::from(cargo_home_config), section, field)
797        {
798            return value;
799        }
800    }
801
802    String::new()
803}
804
805pub fn rust_flags(config: &Config) -> String {
806    const RUSTFLAGS: &str = "RUSTFLAGS";
807    let mut value = config.rustflags.clone().unwrap_or_default();
808    value.push_str(" -Cdebuginfo=2 ");
809    value.push_str("-Cstrip=none ");
810    if !config.avoid_cfg_tarpaulin {
811        value.push_str("--cfg=tarpaulin ");
812    }
813    if config.release {
814        value.push_str("-Cdebug-assertions=off ");
815    }
816    handle_llvm_flags(&mut value, config);
817    lazy_static! {
818        static ref DEBUG_INFO: Regex = Regex::new(r"\-C\s*debuginfo=\d").unwrap();
819        static ref DEAD_CODE: Regex = Regex::new(r"\-C\s*link-dead-code").unwrap();
820    }
821    if let Ok(vtemp) = env::var(RUSTFLAGS) {
822        let temp = DEBUG_INFO.replace_all(&vtemp, " ");
823        if config.no_dead_code {
824            value.push_str(&DEAD_CODE.replace_all(&temp, " "));
825        } else {
826            value.push_str(&temp);
827        }
828    } else {
829        let vtemp = gather_config_field_from_section(config, "build", "rustflags");
830        value.push_str(&DEBUG_INFO.replace_all(&vtemp, " "));
831    }
832
833    deduplicate_flags(&value)
834}
835
836pub fn rustdoc_flags(config: &Config) -> String {
837    const RUSTDOC: &str = "RUSTDOCFLAGS";
838    let common_opts = " -Cdebuginfo=2 --cfg=tarpaulin -Cstrip=none ";
839    let mut value = format!(
840        "{} --persist-doctests {} -Zunstable-options ",
841        common_opts,
842        config.doctest_dir().display()
843    );
844    if let Ok(vtemp) = env::var(RUSTDOC) {
845        if !vtemp.contains("--persist-doctests") {
846            value.push_str(vtemp.as_ref());
847        }
848    } else {
849        let vtemp = gather_config_field_from_section(config, "build", "rustdocflags");
850        value.push_str(&vtemp);
851    }
852    handle_llvm_flags(&mut value, config);
853    deduplicate_flags(&value)
854}
855
856fn deduplicate_flags(flags: &str) -> String {
857    lazy_static! {
858        static ref CFG_FLAG: Regex = Regex::new(r#"\--cfg\s+"#).unwrap();
859        static ref C_FLAG: Regex = Regex::new(r#"\-C\s+"#).unwrap();
860        static ref Z_FLAG: Regex = Regex::new(r#"\-Z\s+"#).unwrap();
861        static ref W_FLAG: Regex = Regex::new(r#"\-W\s+"#).unwrap();
862        static ref A_FLAG: Regex = Regex::new(r#"\-A\s+"#).unwrap();
863        static ref D_FLAG: Regex = Regex::new(r#"\-D\s+"#).unwrap();
864    }
865
866    // Going to remove the excess spaces to make it easier to filter things.
867    let res = CFG_FLAG.replace_all(flags, "--cfg=");
868    let res = C_FLAG.replace_all(&res, "-C");
869    let res = Z_FLAG.replace_all(&res, "-Z");
870    let res = W_FLAG.replace_all(&res, "-W");
871    let res = A_FLAG.replace_all(&res, "-A");
872    let res = D_FLAG.replace_all(&res, "-D");
873
874    let mut flag_set = HashSet::new();
875    let mut result = vec![];
876    for val in res.split_whitespace() {
877        if val.starts_with("--cfg") {
878            if !flag_set.contains(&val) {
879                result.push(val);
880                flag_set.insert(val);
881            }
882        } else {
883            let id = val.split('=').next().unwrap();
884            if !flag_set.contains(id) {
885                flag_set.insert(id);
886                result.push(val);
887            }
888        }
889    }
890    result.join(" ")
891}
892
893fn setup_environment(cmd: &mut Command, config: &Config) {
894    // https://github.com/rust-lang/rust/issues/107447
895    cmd.env("LLVM_PROFILE_FILE", config.root().join(BUILD_PROFRAW));
896    cmd.env("TARPAULIN", "1");
897    let rustflags = "RUSTFLAGS";
898    let value = rust_flags(config);
899    cmd.env(rustflags, value);
900    // doesn't matter if we don't use it
901    let rustdoc = "RUSTDOCFLAGS";
902    let value = rustdoc_flags(config);
903    trace!("Setting RUSTDOCFLAGS='{}'", value);
904    cmd.env(rustdoc, value);
905    if let Ok(bootstrap) = env::var("RUSTC_BOOTSTRAP") {
906        cmd.env("RUSTC_BOOTSTRAP", bootstrap);
907    }
908}
909
910/// Taking the output of cargo version command return true if it's known to be a nightly channel
911/// false otherwise.
912fn is_nightly() -> bool {
913    if let Some(version) = CARGO_VERSION_INFO.as_ref() {
914        version.channel == Channel::Nightly
915    } else {
916        false
917    }
918}
919
920pub fn supports_llvm_coverage() -> bool {
921    if let Some(version) = CARGO_VERSION_INFO.as_ref() {
922        version.supports_llvm_cov()
923    } else {
924        false
925    }
926}
927
928pub fn llvm_coverage_rustflag() -> &'static str {
929    match CARGO_VERSION_INFO.as_ref() {
930        Some(v) if v.minor >= 60 => " -Cinstrument-coverage ",
931        _ => " -Zinstrument-coverage ",
932    }
933}
934
935#[cfg(test)]
936mod tests {
937    use super::*;
938    use toml::toml;
939
940    #[test]
941    fn can_get_libdir() {
942        let path = get_libdir(Some(RunType::Tests)).unwrap();
943        assert!(path.exists(), "{} doesn't exist", path.display());
944    }
945
946    #[test]
947    #[cfg(not(any(windows, target_os = "macos")))]
948    fn check_dead_code_flags() {
949        let mut config = Config::default();
950        config.set_engine(TraceEngine::Ptrace);
951        assert!(rustdoc_flags(&config).contains("link-dead-code"));
952        assert!(rust_flags(&config).contains("link-dead-code"));
953
954        config.no_dead_code = true;
955        assert!(!rustdoc_flags(&config).contains("link-dead-code"));
956        assert!(!rust_flags(&config).contains("link-dead-code"));
957    }
958
959    #[test]
960    fn parse_rustflags_from_toml() {
961        let list_flags = toml! {
962            rustflags = ["--cfg=foo", "--cfg=bar"]
963        };
964        let list_flags = toml::Value::Table(list_flags);
965
966        assert_eq!(
967            look_for_field_in_table(&list_flags, "rustflags"),
968            "--cfg=foo --cfg=bar"
969        );
970
971        let string_flags = toml! {
972            rustflags = "--cfg=bar --cfg=baz"
973        };
974        let string_flags = toml::Value::Table(string_flags);
975
976        assert_eq!(
977            look_for_field_in_table(&string_flags, "rustflags"),
978            "--cfg=bar --cfg=baz"
979        );
980    }
981
982    #[test]
983    fn llvm_cov_compatible_version() {
984        let version = CargoVersionInfo {
985            major: 1,
986            minor: 50,
987            channel: Channel::Nightly,
988        };
989        assert!(version.supports_llvm_cov());
990        let version = CargoVersionInfo {
991            major: 1,
992            minor: 60,
993            channel: Channel::Stable,
994        };
995        assert!(version.supports_llvm_cov());
996    }
997
998    #[test]
999    fn llvm_cov_incompatible_version() {
1000        let mut version = CargoVersionInfo {
1001            major: 1,
1002            minor: 48,
1003            channel: Channel::Stable,
1004        };
1005        assert!(!version.supports_llvm_cov());
1006        version.channel = Channel::Beta;
1007        assert!(!version.supports_llvm_cov());
1008        version.minor = 50;
1009        assert!(!version.supports_llvm_cov());
1010        version.minor = 58;
1011        version.channel = Channel::Stable;
1012        assert!(!version.supports_llvm_cov());
1013    }
1014
1015    #[test]
1016    fn no_duplicate_flags() {
1017        assert_eq!(
1018            deduplicate_flags("--cfg=tarpaulin --cfg tarpaulin"),
1019            "--cfg=tarpaulin"
1020        );
1021        assert_eq!(
1022            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -C link-dead-code"),
1023            "-Clink-dead-code -Zinstrument-coverage"
1024        );
1025        assert_eq!(
1026            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -Zinstrument-coverage"),
1027            "-Clink-dead-code -Zinstrument-coverage"
1028        );
1029        assert_eq!(
1030            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -Cinstrument-coverage"),
1031            "-Clink-dead-code -Zinstrument-coverage -Cinstrument-coverage"
1032        );
1033
1034        assert_eq!(
1035            deduplicate_flags("--cfg=tarpaulin --cfg tarpauline --cfg=tarp"),
1036            "--cfg=tarpaulin --cfg=tarpauline --cfg=tarp"
1037        );
1038    }
1039}