• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xd009642 / tarpaulin / #467

01 May 2024 06:45PM UTC coverage: 74.573% (+0.3%) from 74.24%
#467

push

xd009642
Release 0.29.0

26 of 28 new or added lines in 6 files covered. (92.86%)

6 existing lines in 2 files now uncovered.

2619 of 3512 relevant lines covered (74.57%)

145566.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.38
/src/cargo.rs
1
use crate::config::*;
2
use crate::errors::RunError;
3
use crate::path_utils::{fix_unc_path, get_source_walker};
4
use cargo_metadata::{diagnostic::DiagnosticLevel, CargoOpt, Message, Metadata, MetadataCommand};
5
use lazy_static::lazy_static;
6
use regex::Regex;
7
use serde::{Deserialize, Serialize};
8
use std::collections::{HashMap, HashSet};
9
use std::env;
10
use std::ffi::OsStr;
11
use std::fs::{read_dir, read_to_string, remove_dir_all, remove_file, File};
12
use std::io;
13
use std::io::{BufRead, BufReader};
14
use std::path::{Component, Path, PathBuf};
15
use std::process::{Command, Stdio};
16
use toml::Value;
17
use tracing::{debug, error, info, trace, warn};
18
use walkdir::{DirEntry, WalkDir};
19

20
const BUILD_PROFRAW: &str = "build_rs_cov.profraw";
21

22
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
23
enum Channel {
24
    Stable,
25
    Beta,
26
    Nightly,
27
}
28

29
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
30
struct CargoVersionInfo {
31
    major: usize,
32
    minor: usize,
33
    channel: Channel,
34
}
35

36
impl CargoVersionInfo {
37
    fn supports_llvm_cov(&self) -> bool {
98✔
38
        (self.minor >= 50 && self.channel == Channel::Nightly) || self.minor >= 60
199✔
39
    }
40
}
41

42
#[derive(Clone, Debug, Default)]
43
pub struct CargoOutput {
44
    /// This contains all binaries we want to run to collect coverage from.
45
    pub test_binaries: Vec<TestBinary>,
46
    /// This covers binaries we don't want to run explicitly but may be called as part of tracing
47
    /// execution of other processes.
48
    pub binaries: Vec<PathBuf>,
49
}
50

51
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
52
pub struct TestBinary {
53
    path: PathBuf,
54
    ty: Option<RunType>,
55
    cargo_dir: Option<PathBuf>,
56
    pkg_name: Option<String>,
57
    pkg_version: Option<String>,
58
    pkg_authors: Option<Vec<String>>,
59
    should_panic: bool,
60
    /// Linker paths used when linking the binary, this should be accessed via
61
    /// `Self::has_linker_paths` and `Self::ld_library_path` as there may be interaction with
62
    /// current environment. It's only made pub(crate) for the purpose of testing.
63
    pub(crate) linker_paths: Vec<PathBuf>,
64
}
65

66
#[derive(Clone, Debug)]
67
struct DocTestBinaryMeta {
68
    prefix: String,
69
    line: usize,
70
}
71

72
impl TestBinary {
73
    pub fn new(path: PathBuf, ty: Option<RunType>) -> Self {
191✔
74
        Self {
75
            path,
76
            ty,
77
            pkg_name: None,
78
            pkg_version: None,
79
            pkg_authors: None,
80
            cargo_dir: None,
81
            should_panic: false,
82
            linker_paths: vec![],
191✔
83
        }
84
    }
85

86
    pub fn path(&self) -> &Path {
404✔
87
        &self.path
404✔
88
    }
89

90
    pub fn run_type(&self) -> Option<RunType> {
×
91
        self.ty
×
92
    }
93

94
    pub fn manifest_dir(&self) -> &Option<PathBuf> {
30✔
95
        &self.cargo_dir
30✔
96
    }
97

98
    pub fn pkg_name(&self) -> &Option<String> {
16✔
99
        &self.pkg_name
16✔
100
    }
101

102
    pub fn pkg_version(&self) -> &Option<String> {
16✔
103
        &self.pkg_version
16✔
104
    }
105

106
    pub fn pkg_authors(&self) -> &Option<Vec<String>> {
16✔
107
        &self.pkg_authors
16✔
108
    }
109

110
    pub fn has_linker_paths(&self) -> bool {
32✔
111
        !self.linker_paths.is_empty()
32✔
112
    }
113

114
    pub fn is_test_type(&self) -> bool {
14✔
115
        matches!(self.ty, None | Some(RunType::Tests))
14✔
116
    }
117

118
    /// Convert linker paths to an LD_LIBRARY_PATH.
119
    /// TODO this won't work for windows when it's implemented
120
    pub fn ld_library_path(&self) -> String {
1✔
121
        let mut new_vals = self
1✔
122
            .linker_paths
1✔
123
            .iter()
124
            .map(|x| x.display().to_string())
3✔
125
            .collect::<Vec<String>>()
126
            .join(":");
127
        if let Ok(ld) = env::var("LD_LIBRARY_PATH") {
2✔
128
            new_vals.push(':');
129
            new_vals.push_str(ld.as_str());
130
        }
131
        new_vals
1✔
132
    }
133

134
    /// Should be `false` for normal tests and for doctests either `true` or
135
    /// `false` depending on the test attribute
136
    pub fn should_panic(&self) -> bool {
390✔
137
        self.should_panic
390✔
138
    }
139

140
    /// Convenience function to get the file name of the binary as a string, default string if the
141
    /// path has no filename as this should _never_ happen
142
    pub fn file_name(&self) -> String {
14✔
143
        self.path
14✔
144
            .file_name()
145
            .map(|x| x.to_string_lossy().to_string())
42✔
146
            .unwrap_or_default()
147
    }
148
}
149

150
impl DocTestBinaryMeta {
151
    fn new<P: AsRef<Path>>(test: P) -> Option<Self> {
66✔
152
        if let Some(Component::Normal(folder)) = test.as_ref().components().nth_back(1) {
132✔
153
            let temp = folder.to_string_lossy();
66✔
154
            let file_end = temp.rfind("rs").map(|i| i + 2)?;
264✔
155
            let end = temp.rfind('_')?;
66✔
156
            if end > file_end + 1 {
×
157
                let line = temp[(file_end + 1)..end].parse::<usize>().ok()?;
132✔
158
                Some(Self {
66✔
159
                    prefix: temp[..file_end].to_string(),
66✔
160
                    line,
66✔
161
                })
162
            } else {
163
                None
×
164
            }
165
        } else {
166
            None
×
167
        }
168
    }
169
}
170

171
lazy_static! {
172
    static ref CARGO_VERSION_INFO: Option<CargoVersionInfo> = {
173
        let version_info = Regex::new(
174
            r"cargo (\d)\.(\d+)\.\d+([\-betanightly]*)(\.[[:alnum:]]+)?",
175
        )
176
        .unwrap();
177
        Command::new("cargo")
178
            .arg("--version")
179
            .output()
180
            .map(|x| {
4✔
181
                let s = String::from_utf8_lossy(&x.stdout);
4✔
182
                if let Some(cap) = version_info.captures(&s) {
8✔
183
                    let major = cap[1].parse().unwrap();
184
                    let minor = cap[2].parse().unwrap();
185
                    // We expect a string like `cargo 1.50.0-nightly (a0f433460 2020-02-01)
186
                    // the version number either has `-nightly` `-beta` or empty for stable
187
                    let channel = match &cap[3] {
4✔
188
                        "-nightly" => Channel::Nightly,
4✔
189
                        "-beta" => Channel::Beta,
×
190
                        _ => Channel::Stable,
×
191
                    };
192
                    Some(CargoVersionInfo {
193
                        major,
194
                        minor,
195
                        channel,
196
                    })
197
                } else {
198
                    None
×
199
                }
200
            })
201
            .unwrap_or(None)
202
    };
203
}
204

205
pub fn get_tests(config: &Config) -> Result<CargoOutput, RunError> {
164✔
206
    let mut result = CargoOutput::default();
164✔
207
    if config.force_clean() {
164✔
208
        let cleanup_dir = if config.release {
4✔
209
            config.target_dir().join("release")
×
210
        } else {
211
            config.target_dir().join("debug")
2✔
212
        };
213
        info!("Cleaning project");
2✔
214
        if cleanup_dir.exists() {
2✔
215
            if let Err(e) = remove_dir_all(cleanup_dir) {
×
216
                error!("Cargo clean failed: {e}");
×
217
            }
218
        }
219
    }
220
    let man_binding = config.manifest();
164✔
221
    let manifest = man_binding.as_path().to_str().unwrap_or("Cargo.toml");
164✔
222
    let metadata = MetadataCommand::new()
162✔
223
        .manifest_path(manifest)
224
        .features(CargoOpt::AllFeatures)
225
        .exec()
226
        .map_err(|e| RunError::Cargo(e.to_string()))?;
4✔
227

228
    for ty in &config.run_types {
234✔
229
        run_cargo(&metadata, manifest, config, Some(*ty), &mut result)?;
4✔
230
    }
231
    if config.has_named_tests() {
158✔
232
        run_cargo(&metadata, manifest, config, None, &mut result)?;
4✔
233
    } else if config.run_types.is_empty() {
154✔
234
        let ty = if config.command == Mode::Test {
244✔
235
            Some(RunType::Tests)
118✔
236
        } else {
237
            None
4✔
238
        };
239
        run_cargo(&metadata, manifest, config, ty, &mut result)?;
4✔
240
    }
241
    // Only matters for llvm cov and who knows, one day may not be needed
242
    let _ = remove_file(config.root().join(BUILD_PROFRAW));
154✔
243
    Ok(result)
154✔
244
}
245

246
fn run_cargo(
164✔
247
    metadata: &Metadata,
248
    manifest: &str,
249
    config: &Config,
250
    ty: Option<RunType>,
251
    result: &mut CargoOutput,
252
) -> Result<(), RunError> {
253
    let mut cmd = create_command(manifest, config, ty);
164✔
254
    if ty != Some(RunType::Doctests) {
310✔
255
        cmd.stdout(Stdio::piped());
146✔
256
    } else {
257
        clean_doctest_folder(config.doctest_dir());
18✔
258
        cmd.stdout(Stdio::null());
18✔
259
    }
260
    trace!("Running command {:?}", cmd);
252✔
261
    let mut child = cmd.spawn().map_err(|e| RunError::Cargo(e.to_string()))?;
328✔
262
    let update_from = result.test_binaries.len();
263
    let mut paths = vec![];
264

265
    if ty != Some(RunType::Doctests) {
266
        let mut package_ids = vec![None; result.test_binaries.len()];
146✔
267
        let reader = std::io::BufReader::new(child.stdout.take().unwrap());
146✔
268
        let mut error = None;
146✔
269
        for msg in Message::parse_stream(reader) {
846✔
270
            match msg {
52✔
271
                Ok(Message::CompilerArtifact(art)) => {
466✔
272
                    if let Some(path) = art.executable.as_ref() {
642✔
273
                        if !art.profile.test && config.command == Mode::Test {
14✔
274
                            result.binaries.push(PathBuf::from(path));
8✔
275
                            continue;
8✔
276
                        }
277
                        result
168✔
278
                            .test_binaries
168✔
279
                            .push(TestBinary::new(fix_unc_path(path.as_std_path()), ty));
168✔
280
                        package_ids.push(Some(art.package_id.clone()));
168✔
281
                    }
282
                }
283
                Ok(Message::CompilerMessage(m)) => match m.message.level {
38✔
284
                    DiagnosticLevel::Error | DiagnosticLevel::Ice => {
285
                        let msg = if let Some(rendered) = m.message.rendered {
6✔
286
                            rendered
2✔
287
                        } else {
288
                            format!("{}: {}", m.target.name, m.message.message)
×
289
                        };
290
                        error = Some(RunError::TestCompile(msg));
291
                        break;
292
                    }
293
                    _ => {}
36✔
294
                },
295
                Ok(Message::BuildScriptExecuted(bs))
×
296
                    if !(bs.linked_libs.is_empty() || bs.linked_paths.is_empty()) =>
54✔
297
                {
298
                    let temp_paths = bs.linked_paths.iter().filter_map(|x| {
×
299
                        if x.as_std_path().exists() {
×
300
                            Some(x.as_std_path().to_path_buf())
×
301
                        } else if let Some(index) = x.as_str().find('=') {
×
302
                            Some(PathBuf::from(&x.as_str()[(index + 1)..]))
×
303
                        } else {
304
                            warn!("Couldn't resolve linker path: {}", x.as_str());
×
305
                            None
×
306
                        }
307
                    });
308
                    for p in temp_paths {
×
309
                        if !paths.contains(&p) {
×
310
                            paths.push(p);
×
311
                        }
312
                    }
313
                }
314
                Err(e) => {
×
315
                    error!("Error parsing cargo messages {e}");
×
316
                }
317
                _ => {}
196✔
318
            }
319
        }
320
        debug!("Linker paths: {:?}", paths);
234✔
321
        for bin in result.test_binaries.iter_mut().skip(update_from) {
482✔
322
            bin.linker_paths = paths.clone();
168✔
323
        }
324
        let status = child.wait().unwrap();
146✔
325
        if let Some(error) = error {
148✔
326
            return Err(error);
2✔
327
        }
328
        if !status.success() {
144✔
329
            return Err(RunError::Cargo("cargo run failed".to_string()));
2✔
330
        };
331
        for (res, package) in result
168✔
332
            .test_binaries
333
            .iter_mut()
334
            .zip(package_ids.iter())
335
            .filter(|(_, b)| b.is_some())
170✔
336
        {
337
            if let Some(package) = package {
336✔
338
                let package = &metadata[package];
339
                res.cargo_dir = package
340
                    .manifest_path
341
                    .parent()
342
                    .map(|x| fix_unc_path(x.as_std_path()));
168✔
343
                res.pkg_name = Some(package.name.clone());
344
                res.pkg_version = Some(package.version.to_string());
345
                res.pkg_authors = Some(package.authors.clone());
346
            }
347
        }
348
        child.wait().map_err(|e| RunError::Cargo(e.to_string()))?;
284✔
349
    } else {
350
        // need to wait for compiling to finish before getting doctests
351
        // also need to wait with output to ensure the stdout buffer doesn't fill up
352
        let out = child
36✔
353
            .wait_with_output()
354
            .map_err(|e| RunError::Cargo(e.to_string()))?;
18✔
355
        if !out.status.success() {
356
            error!("Building doctests failed");
6✔
357
            return Err(RunError::Cargo("Building doctest failed".to_string()));
4✔
358
        }
359
        let walker = WalkDir::new(config.doctest_dir()).into_iter();
14✔
360
        let dir_entries = walker
14✔
361
            .filter_map(Result::ok)
14✔
362
            .filter(|e| matches!(e.metadata(), Ok(ref m) if m.is_file() && m.len() != 0))
246✔
363
            .filter(|e| e.path().extension() != Some(OsStr::new("pdb")))
50✔
364
            .filter(|e| {
36✔
365
                !e.path()
22✔
366
                    .components()
22✔
367
                    .any(|x| x.as_os_str().to_string_lossy().contains("dSYM"))
330✔
368
            })
369
            .collect::<Vec<_>>();
370

371
        let should_panics = get_attribute_candidates(&dir_entries, config, "should_panic");
14✔
372
        let no_runs = get_attribute_candidates(&dir_entries, config, "no_run");
14✔
373
        for dt in &dir_entries {
58✔
374
            let mut tb = TestBinary::new(fix_unc_path(dt.path()), ty);
375

376
            if let Some(meta) = DocTestBinaryMeta::new(dt.path()) {
22✔
377
                if no_runs
378
                    .get(&meta.prefix)
379
                    .map(|x| x.contains(&meta.line))
22✔
380
                    .unwrap_or(false)
381
                {
382
                    info!("Skipping no_run doctest: {}", dt.path().display());
4✔
383
                    continue;
2✔
384
                }
385
                if let Some(lines) = should_panics.get(&meta.prefix) {
60✔
386
                    tb.should_panic |= lines.contains(&meta.line);
20✔
387
                }
388
            }
389
            let mut current_dir = dt.path();
20✔
390
            loop {
100✔
391
                if current_dir.is_dir() && current_dir.join("Cargo.toml").exists() {
180✔
392
                    tb.cargo_dir = Some(fix_unc_path(current_dir));
20✔
393
                    break;
20✔
394
                }
395
                match current_dir.parent() {
80✔
396
                    Some(s) => {
80✔
397
                        current_dir = s;
80✔
398
                    }
399
                    None => break,
×
400
                }
401
            }
402
            result.test_binaries.push(tb);
20✔
403
        }
404
    }
405
    Ok(())
156✔
406
}
407

408
fn convert_to_prefix(p: &Path) -> Option<String> {
60✔
409
    // Need to go from directory after last one with Cargo.toml
410
    let convert_name = |p: &Path| {
272✔
411
        if let Some(s) = p.file_name() {
364✔
412
            s.to_str().map(|x| x.replace('.', "_")).unwrap_or_default()
152✔
413
        } else {
414
            String::new()
60✔
415
        }
416
    };
417
    let mut buffer = vec![convert_name(p)];
60✔
418
    let mut parent = p.parent();
60✔
419
    while let Some(path_temp) = parent {
516✔
420
        buffer.insert(0, convert_name(path_temp));
152✔
421
        parent = path_temp.parent();
152✔
422
    }
423
    if buffer.is_empty() {
60✔
424
        None
×
425
    } else {
426
        Some(buffer.join("_"))
60✔
427
    }
428
}
429

430
fn is_prefix_match(prefix: &str, entry: &Path) -> bool {
60✔
431
    convert_to_prefix(entry)
60✔
432
        .map(|s| s.contains(prefix))
180✔
433
        .unwrap_or(false)
434
}
435

436
/// This returns a map of the string prefixes for the file in the doc test and a list of lines
437
/// which contain the string `should_panic` it makes no guarantees that all these lines are a
438
/// doctest attribute showing panic behaviour (but some of them will be)
439
///
440
/// Currently all doctest files take the pattern of `{name}_{line}_{number}` where name is the
441
/// path to the file with directory separators and dots replaced with underscores. Therefore
442
/// each name could potentially map to many files as `src_some_folder_foo_rs_1_1` could go to
443
/// `src/some/folder_foo.rs` or `src/some/folder/foo.rs` here we're going to work on a heuristic
444
/// that any matching file is good because we can't do any better
445
///
446
/// As of some point in June 2023 the naming convention has changed to include the package name in
447
/// the generated name which reduces collisions. Before it was done relative to the workspace
448
/// package folder not the workspace root.
449
fn get_attribute_candidates(
28✔
450
    tests: &[DirEntry],
451
    config: &Config,
452
    attribute: &str,
453
) -> HashMap<String, Vec<usize>> {
454
    let mut result = HashMap::new();
28✔
455
    let mut checked_files = HashSet::new();
28✔
456
    let root = config.root();
28✔
457
    for test in tests {
116✔
458
        if let Some(test_binary) = DocTestBinaryMeta::new(test.path()) {
44✔
459
            for dir_entry in get_source_walker(config) {
60✔
460
                let path = dir_entry.path();
60✔
461
                if path.is_file() {
60✔
462
                    if let Some(p) = path_relative_from(path, &root) {
120✔
463
                        if is_prefix_match(&test_binary.prefix, &p) && !checked_files.contains(path)
44✔
464
                        {
465
                            checked_files.insert(path.to_path_buf());
32✔
466
                            let lines = find_str_in_file(path, attribute).unwrap_or_default();
32✔
467
                            if !result.contains_key(&test_binary.prefix) {
64✔
468
                                result.insert(test_binary.prefix.clone(), lines);
32✔
469
                            } else if let Some(current_lines) = result.get_mut(&test_binary.prefix)
32✔
470
                            {
471
                                current_lines.extend_from_slice(&lines);
472
                            }
473
                        }
474
                    }
475
                }
476
            }
477
        } else {
478
            warn!(
×
479
                "Invalid characters in name of doctest {}",
480
                test.path().display()
×
481
            );
482
        }
483
    }
484
    result
28✔
485
}
486

487
fn find_str_in_file(file: &Path, value: &str) -> io::Result<Vec<usize>> {
32✔
488
    let f = File::open(file)?;
64✔
489
    let reader = BufReader::new(f);
490
    let lines = reader
491
        .lines()
492
        .enumerate()
493
        .filter(|(_, l)| l.as_ref().map(|x| x.contains(value)).unwrap_or(false))
1,308✔
494
        .map(|(i, _)| i + 1) // Move from line index to line number
12✔
495
        .collect();
496
    Ok(lines)
497
}
498

499
fn create_command(manifest_path: &str, config: &Config, ty: Option<RunType>) -> Command {
164✔
500
    let mut test_cmd = Command::new("cargo");
164✔
501
    let bootstrap = matches!(env::var("RUSTC_BOOTSTRAP").as_deref(), Ok("1"));
332✔
502
    let override_toolchain = if cfg!(windows) {
503
        if env::var("PATH").unwrap_or_default().contains(".rustup") {
×
504
            // So the specific cargo we're using is in the path var so rustup toolchains won't
505
            // work. This only started happening recently so special casing it for older versions
506
            env::remove_var("RUSTUP_TOOLCHAIN");
×
507
            false
×
508
        } else {
509
            true
×
510
        }
511
    } else {
512
        true
164✔
513
    };
514
    if ty == Some(RunType::Doctests) {
515
        if override_toolchain {
18✔
516
            if let Some(toolchain) = env::var("RUSTUP_TOOLCHAIN")
36✔
517
                .ok()
518
                .filter(|t| t.starts_with("nightly") || bootstrap)
72✔
519
            {
520
                test_cmd.args([format!("+{toolchain}").as_str()]);
18✔
521
            } else if !bootstrap && !is_nightly() {
18✔
NEW
522
                test_cmd.args(["+nightly"]);
×
523
            }
524
        }
525
        test_cmd.args(["test"]);
18✔
526
    } else {
527
        if override_toolchain {
146✔
528
            if let Ok(toolchain) = env::var("RUSTUP_TOOLCHAIN") {
292✔
529
                test_cmd.arg(format!("+{toolchain}"));
530
            }
531
        }
532
        if config.command == Mode::Test {
286✔
533
            test_cmd.args(["test", "--no-run"]);
140✔
534
        } else {
535
            test_cmd.arg("build");
6✔
536
        }
537
    }
538
    test_cmd.args(["--message-format", "json", "--manifest-path", manifest_path]);
164✔
539
    if let Some(ty) = ty {
156✔
540
        match ty {
541
            RunType::Tests => test_cmd.arg("--tests"),
128✔
542
            RunType::Doctests => test_cmd.arg("--doc"),
18✔
543
            RunType::Benchmarks => test_cmd.arg("--benches"),
×
544
            RunType::Examples => test_cmd.arg("--examples"),
8✔
545
            RunType::AllTargets => test_cmd.arg("--all-targets"),
2✔
546
            RunType::Lib => test_cmd.arg("--lib"),
×
547
            RunType::Bins => test_cmd.arg("--bins"),
×
548
        };
549
    } else {
550
        for test in &config.test_names {
14✔
551
            test_cmd.arg("--test");
2✔
552
            test_cmd.arg(test);
2✔
553
        }
554
        for test in &config.bin_names {
8✔
555
            test_cmd.arg("--bin");
×
556
            test_cmd.arg(test);
×
557
        }
558
        for test in &config.example_names {
14✔
559
            test_cmd.arg("--example");
2✔
560
            test_cmd.arg(test);
2✔
561
        }
562
        for test in &config.bench_names {
8✔
563
            test_cmd.arg("--bench");
×
564
            test_cmd.arg(test);
×
565
        }
566
    }
567
    init_args(&mut test_cmd, config);
164✔
568
    setup_environment(&mut test_cmd, config);
164✔
569
    test_cmd
164✔
570
}
571

572
fn init_args(test_cmd: &mut Command, config: &Config) {
164✔
573
    if config.debug {
164✔
574
        test_cmd.arg("-vvv");
×
575
    } else if config.verbose {
184✔
576
        test_cmd.arg("-v");
20✔
577
    }
578
    if config.locked {
164✔
579
        test_cmd.arg("--locked");
×
580
    }
581
    if config.frozen {
164✔
582
        test_cmd.arg("--frozen");
×
583
    }
584
    if config.no_fail_fast {
166✔
585
        test_cmd.arg("--no-fail-fast");
2✔
586
    }
587
    if let Some(profile) = config.profile.as_ref() {
164✔
588
        test_cmd.arg("--profile");
589
        test_cmd.arg(profile);
590
    }
591
    if let Some(jobs) = config.jobs {
164✔
592
        test_cmd.arg("--jobs");
593
        test_cmd.arg(jobs.to_string());
594
    }
595
    if let Some(features) = config.features.as_ref() {
168✔
596
        test_cmd.arg("--features");
597
        test_cmd.arg(features);
598
    }
599
    if config.all_features {
164✔
600
        test_cmd.arg("--all-features");
×
601
    }
602
    if config.no_default_features {
164✔
603
        test_cmd.arg("--no-default-features");
×
604
    }
605
    if config.all {
172✔
606
        test_cmd.arg("--workspace");
8✔
607
    }
608
    if config.release {
164✔
609
        test_cmd.arg("--release");
×
610
    }
611
    config.packages.iter().for_each(|package| {
176✔
612
        test_cmd.arg("--package");
12✔
613
        test_cmd.arg(package);
12✔
614
    });
615
    config.exclude.iter().for_each(|package| {
4✔
616
        test_cmd.arg("--exclude");
4✔
617
        test_cmd.arg(package);
4✔
618
    });
619
    test_cmd.arg("--color");
620
    test_cmd.arg(config.color.to_string().to_ascii_lowercase());
621
    if let Some(target) = config.target.as_ref() {
×
622
        test_cmd.args(["--target", target]);
623
    }
624
    let args = vec![
164✔
625
        "--target-dir".to_string(),
164✔
626
        format!("{}", config.target_dir().display()),
164✔
627
    ];
628
    test_cmd.args(args);
164✔
629
    if config.offline {
164✔
630
        test_cmd.arg("--offline");
×
631
    }
632
    for feat in &config.unstable_features {
164✔
633
        test_cmd.arg(format!("-Z{feat}"));
×
634
    }
635
    if config.command == Mode::Test && !config.varargs.is_empty() {
324✔
636
        let mut args = vec!["--".to_string()];
2✔
637
        args.extend_from_slice(&config.varargs);
2✔
638
        test_cmd.args(args);
2✔
639
    }
640
}
641

642
/// Old doc tests that no longer exist or where the line have changed can persist so delete them to
643
/// avoid confusing the results
644
fn clean_doctest_folder<P: AsRef<Path>>(doctest_dir: P) {
18✔
645
    if let Ok(rd) = read_dir(doctest_dir.as_ref()) {
22✔
646
        rd.flat_map(Result::ok)
×
647
            .filter(|e| {
4✔
648
                e.path()
4✔
649
                    .components()
4✔
650
                    .next_back()
4✔
651
                    .map(|e| e.as_os_str().to_string_lossy().contains("rs"))
12✔
652
                    .unwrap_or(false)
4✔
653
            })
654
            .for_each(|e| {
4✔
655
                if let Err(err) = remove_dir_all(e.path()) {
4✔
656
                    warn!("Failed to delete {}: {}", e.path().display(), err);
×
657
                }
658
            });
659
    }
660
}
661

662
fn handle_llvm_flags(value: &mut String, config: &Config) {
530✔
663
    if config.engine() == TraceEngine::Llvm {
564✔
664
        value.push_str(llvm_coverage_rustflag());
34✔
665
    }
666
    if cfg!(not(windows)) && !config.no_dead_code {
1,588✔
667
        value.push_str(" -Clink-dead-code ");
528✔
668
    }
669
}
670

671
fn look_for_field_in_table(value: &Value, field: &str) -> String {
20✔
672
    let table = value.as_table().unwrap();
20✔
673

674
    if let Some(rustflags) = table.get(field) {
38✔
675
        if rustflags.is_array() {
676
            let vec_of_flags: Vec<String> = rustflags
17✔
677
                .as_array()
678
                .unwrap()
679
                .iter()
680
                .filter_map(Value::as_str)
17✔
681
                .map(ToString::to_string)
17✔
682
                .collect();
683

684
            vec_of_flags.join(" ")
17✔
685
        } else if rustflags.is_str() {
1✔
686
            rustflags.as_str().unwrap().to_string()
1✔
687
        } else {
688
            String::new()
×
689
        }
690
    } else {
691
        String::new()
2✔
692
    }
693
}
694

695
fn look_for_field_in_file(path: &Path, section: &str, field: &str) -> Option<String> {
650✔
696
    if let Ok(contents) = read_to_string(path) {
668✔
697
        let value = contents.parse::<Value>().ok()?;
18✔
698

699
        let value: Vec<String> = value
18✔
700
            .as_table()?
701
            .into_iter()
702
            .map(|(s, v)| {
36✔
703
                if s.as_str() == section {
18✔
704
                    look_for_field_in_table(v, field)
18✔
705
                } else {
706
                    String::new()
×
707
                }
708
            })
709
            .collect();
710

711
        Some(value.join(" "))
18✔
712
    } else {
713
        None
632✔
714
    }
715
}
716

717
fn look_for_field_in_section(path: &Path, section: &str, field: &str) -> Option<String> {
326✔
718
    let mut config_path = path.join("config");
326✔
719

720
    let value = look_for_field_in_file(&config_path, section, field);
326✔
721
    if value.is_some() {
326✔
722
        return value;
2✔
723
    }
724

725
    config_path.pop();
324✔
726
    config_path.push("config.toml");
324✔
727

728
    let value = look_for_field_in_file(&config_path, section, field);
324✔
729
    if value.is_some() {
324✔
730
        return value;
16✔
731
    }
732

733
    None
308✔
734
}
735

736
fn build_config_path(base: impl AsRef<Path>) -> PathBuf {
172✔
737
    let mut config_path = PathBuf::from(base.as_ref());
172✔
738
    config_path.push(base);
172✔
739
    config_path.push(".cargo");
172✔
740

741
    config_path
172✔
742
}
743

744
fn gather_config_field_from_section(config: &Config, section: &str, field: &str) -> String {
172✔
745
    if let Some(value) =
18✔
746
        look_for_field_in_section(&build_config_path(config.root()), section, field)
172✔
747
    {
748
        return value;
749
    }
750

751
    if let Ok(cargo_home_config) = env::var("CARGO_HOME") {
308✔
752
        if let Some(value) =
×
753
            look_for_field_in_section(&PathBuf::from(cargo_home_config), section, field)
754
        {
755
            return value;
×
756
        }
757
    }
758

759
    String::new()
154✔
760
}
761

762
pub fn rust_flags(config: &Config) -> String {
364✔
763
    const RUSTFLAGS: &str = "RUSTFLAGS";
764
    let mut value = config.rustflags.clone().unwrap_or_default();
364✔
765
    value.push_str(" -Cdebuginfo=2 ");
364✔
766
    value.push_str("-Cstrip=none ");
364✔
767
    if !config.avoid_cfg_tarpaulin {
728✔
768
        value.push_str("--cfg=tarpaulin ");
364✔
769
    }
770
    if config.release {
364✔
771
        value.push_str("-Cdebug-assertions=off ");
×
772
    }
773
    handle_llvm_flags(&mut value, config);
364✔
774
    lazy_static! {
775
        static ref DEBUG_INFO: Regex = Regex::new(r"\-C\s*debuginfo=\d").unwrap();
776
        static ref DEAD_CODE: Regex = Regex::new(r"\-C\s*link-dead-code").unwrap();
777
    }
778
    if let Ok(vtemp) = env::var(RUSTFLAGS) {
356✔
779
        let temp = DEBUG_INFO.replace_all(&vtemp, " ");
780
        if config.no_dead_code {
1✔
781
            value.push_str(&DEAD_CODE.replace_all(&temp, " "));
1✔
782
        } else {
783
            value.push_str(&temp);
355✔
784
        }
785
    } else {
786
        let vtemp = gather_config_field_from_section(config, "build", "rustflags");
8✔
787
        value.push_str(&DEBUG_INFO.replace_all(&vtemp, " "));
8✔
788
    }
789

790
    deduplicate_flags(&value)
364✔
791
}
792

793
pub fn rustdoc_flags(config: &Config) -> String {
166✔
794
    const RUSTDOC: &str = "RUSTDOCFLAGS";
795
    let common_opts = " -Cdebuginfo=2 --cfg=tarpaulin -Cstrip=none ";
166✔
796
    let mut value = format!(
166✔
797
        "{} --persist-doctests {} -Zunstable-options ",
798
        common_opts,
166✔
799
        config.doctest_dir().display()
166✔
800
    );
801
    if let Ok(vtemp) = env::var(RUSTDOC) {
168✔
802
        if !vtemp.contains("--persist-doctests") {
2✔
803
            value.push_str(vtemp.as_ref());
2✔
804
        }
805
    } else {
806
        let vtemp = gather_config_field_from_section(config, "build", "rustdocflags");
164✔
807
        value.push_str(&vtemp);
164✔
808
    }
809
    handle_llvm_flags(&mut value, config);
166✔
810
    deduplicate_flags(&value)
166✔
811
}
812

813
fn deduplicate_flags(flags: &str) -> String {
535✔
814
    lazy_static! {
535✔
815
        static ref CFG_FLAG: Regex = Regex::new(r#"\--cfg\s+"#).unwrap();
535✔
816
        static ref C_FLAG: Regex = Regex::new(r#"\-C\s+"#).unwrap();
535✔
817
        static ref Z_FLAG: Regex = Regex::new(r#"\-Z\s+"#).unwrap();
535✔
818
        static ref W_FLAG: Regex = Regex::new(r#"\-W\s+"#).unwrap();
535✔
819
        static ref A_FLAG: Regex = Regex::new(r#"\-A\s+"#).unwrap();
535✔
820
        static ref D_FLAG: Regex = Regex::new(r#"\-D\s+"#).unwrap();
535✔
821
    }
822

823
    // Going to remove the excess spaces to make it easier to filter things.
824
    let res = CFG_FLAG.replace_all(flags, "--cfg=");
535✔
825
    let res = C_FLAG.replace_all(&res, "-C");
535✔
826
    let res = Z_FLAG.replace_all(&res, "-Z");
535✔
827
    let res = W_FLAG.replace_all(&res, "-W");
535✔
828
    let res = A_FLAG.replace_all(&res, "-A");
535✔
829
    let res = D_FLAG.replace_all(&res, "-D");
535✔
830

831
    let mut flag_set = HashSet::new();
535✔
832
    let mut result = vec![];
535✔
833
    for val in res.split_whitespace() {
4,620✔
834
        if val.starts_with("--cfg") {
4,085✔
835
            if !flag_set.contains(&val) {
1,461✔
836
                result.push(val);
556✔
837
                flag_set.insert(val);
556✔
838
            }
839
        } else {
840
            let id = val.split('=').next().unwrap();
3,180✔
841
            if !flag_set.contains(id) {
5,639✔
842
                flag_set.insert(id);
2,459✔
843
                result.push(val);
2,459✔
844
            }
845
        }
846
    }
847
    result.join(" ")
535✔
848
}
849

850
fn setup_environment(cmd: &mut Command, config: &Config) {
164✔
851
    // https://github.com/rust-lang/rust/issues/107447
852
    cmd.env("LLVM_PROFILE_FILE", config.root().join(BUILD_PROFRAW));
164✔
853
    cmd.env("TARPAULIN", "1");
164✔
854
    let rustflags = "RUSTFLAGS";
164✔
855
    let value = rust_flags(config);
164✔
856
    cmd.env(rustflags, value);
164✔
857
    // doesn't matter if we don't use it
858
    let rustdoc = "RUSTDOCFLAGS";
164✔
859
    let value = rustdoc_flags(config);
164✔
860
    trace!("Setting RUSTDOCFLAGS='{}'", value);
252✔
861
    cmd.env(rustdoc, value);
164✔
862
    if let Ok(bootstrap) = env::var("RUSTC_BOOTSTRAP") {
2✔
863
        cmd.env("RUSTC_BOOTSTRAP", bootstrap);
864
    }
865
}
866

867
/// Taking the output of cargo version command return true if it's known to be a nightly channel
868
/// false otherwise.
869
fn is_nightly() -> bool {
×
870
    if let Some(version) = CARGO_VERSION_INFO.as_ref() {
×
871
        version.channel == Channel::Nightly
872
    } else {
873
        false
×
874
    }
875
}
876

877
pub fn supports_llvm_coverage() -> bool {
92✔
878
    if let Some(version) = CARGO_VERSION_INFO.as_ref() {
184✔
879
        version.supports_llvm_cov()
880
    } else {
881
        false
×
882
    }
883
}
884

885
pub fn llvm_coverage_rustflag() -> &'static str {
34✔
886
    match CARGO_VERSION_INFO.as_ref() {
34✔
887
        Some(v) if v.minor >= 60 => " -Cinstrument-coverage ",
102✔
888
        _ => " -Zinstrument-coverage ",
×
889
    }
890
}
891

892
#[cfg(test)]
893
mod tests {
894
    use super::*;
895
    use toml::toml;
896

897
    #[test]
898
    #[cfg(not(windows))]
899
    fn check_dead_code_flags() {
900
        let mut config = Config::default();
901
        assert!(rustdoc_flags(&config).contains("link-dead-code"));
902
        assert!(rust_flags(&config).contains("link-dead-code"));
903

904
        config.no_dead_code = true;
905
        assert!(!rustdoc_flags(&config).contains("link-dead-code"));
906
        assert!(!rust_flags(&config).contains("link-dead-code"));
907
    }
908

909
    #[test]
910
    fn parse_rustflags_from_toml() {
911
        let list_flags = toml! {
912
            rustflags = ["--cfg=foo", "--cfg=bar"]
913
        };
914
        let list_flags = toml::Value::Table(list_flags);
915

916
        assert_eq!(
917
            look_for_field_in_table(&list_flags, "rustflags"),
918
            "--cfg=foo --cfg=bar"
919
        );
920

921
        let string_flags = toml! {
922
            rustflags = "--cfg=bar --cfg=baz"
923
        };
924
        let string_flags = toml::Value::Table(string_flags);
925

926
        assert_eq!(
927
            look_for_field_in_table(&string_flags, "rustflags"),
928
            "--cfg=bar --cfg=baz"
929
        );
930
    }
931

932
    #[test]
933
    fn llvm_cov_compatible_version() {
934
        let version = CargoVersionInfo {
935
            major: 1,
936
            minor: 50,
937
            channel: Channel::Nightly,
938
        };
939
        assert!(version.supports_llvm_cov());
940
        let version = CargoVersionInfo {
941
            major: 1,
942
            minor: 60,
943
            channel: Channel::Stable,
944
        };
945
        assert!(version.supports_llvm_cov());
946
    }
947

948
    #[test]
949
    fn llvm_cov_incompatible_version() {
950
        let mut version = CargoVersionInfo {
951
            major: 1,
952
            minor: 48,
953
            channel: Channel::Stable,
954
        };
955
        assert!(!version.supports_llvm_cov());
956
        version.channel = Channel::Beta;
957
        assert!(!version.supports_llvm_cov());
958
        version.minor = 50;
959
        assert!(!version.supports_llvm_cov());
960
        version.minor = 58;
961
        version.channel = Channel::Stable;
962
        assert!(!version.supports_llvm_cov());
963
    }
964

965
    #[test]
966
    fn no_duplicate_flags() {
967
        assert_eq!(
968
            deduplicate_flags("--cfg=tarpaulin --cfg tarpaulin"),
969
            "--cfg=tarpaulin"
970
        );
971
        assert_eq!(
972
            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -C link-dead-code"),
973
            "-Clink-dead-code -Zinstrument-coverage"
974
        );
975
        assert_eq!(
976
            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -Zinstrument-coverage"),
977
            "-Clink-dead-code -Zinstrument-coverage"
978
        );
979
        assert_eq!(
980
            deduplicate_flags("-Clink-dead-code -Zinstrument-coverage -Cinstrument-coverage"),
981
            "-Clink-dead-code -Zinstrument-coverage -Cinstrument-coverage"
982
        );
983

984
        assert_eq!(
985
            deduplicate_flags("--cfg=tarpaulin --cfg tarpauline --cfg=tarp"),
986
            "--cfg=tarpaulin --cfg=tarpauline --cfg=tarp"
987
        );
988
    }
989
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc