1use self::parse::*;
2pub use self::types::*;
3use crate::path_utils::fix_unc_path;
4use crate::{args::ConfigArgs, cargo::supports_llvm_coverage};
5use cargo_metadata::{Metadata, MetadataCommand};
6#[cfg(feature = "coveralls")]
7use coveralls_api::CiService;
8use glob::Pattern;
9use humantime_serde::deserialize as humantime_serde;
10use indexmap::IndexMap;
11use serde::{Deserialize, Serialize};
12use std::cell::{Ref, RefCell};
13use std::collections::HashSet;
14use std::env;
15use std::fs;
16use std::io::{Error, ErrorKind};
17use std::path::{Path, PathBuf};
18use std::time::Duration;
19use tracing::{error, info, warn};
20
21mod parse;
22pub mod types;
23
24#[derive(Debug)]
25pub struct ConfigWrapper(pub Vec<Config>);
26
27#[derive(Debug, Clone, Deserialize, Serialize)]
29#[serde(default)]
30pub struct Config {
31 pub name: String,
32 #[serde(rename = "manifest-path")]
34 manifest: PathBuf,
35 pub config: Option<PathBuf>,
37 root: Option<PathBuf>,
39 #[serde(rename = "ignored")]
41 pub run_ignored: bool,
42 #[serde(rename = "ignore-panics")]
44 pub ignore_panics: bool,
45 #[serde(rename = "force-clean")]
47 force_clean: bool,
48 #[serde(rename = "skip-clean")]
50 skip_clean: bool,
51 pub verbose: bool,
53 pub debug: bool,
55 #[serde(rename = "dump-traces")]
57 pub dump_traces: bool,
58 pub count: bool,
60 #[serde(rename = "line")]
62 pub line_coverage: bool,
63 #[serde(rename = "branch")]
65 pub branch_coverage: bool,
66 #[serde(rename = "output-dir")]
68 pub output_directory: Option<PathBuf>,
69 pub coveralls: Option<String>,
71 #[serde(rename = "ciserver", deserialize_with = "deserialize_ci_server")]
73 #[cfg(feature = "coveralls")]
74 pub ci_tool: Option<CiService>,
75 #[serde(rename = "report-uri")]
79 pub report_uri: Option<String>,
80 #[serde(rename = "forward")]
83 pub forward_signals: bool,
84 #[serde(rename = "no-dead-code")]
86 pub no_dead_code: bool,
87 #[serde(rename = "all-features")]
89 pub all_features: bool,
90 #[serde(rename = "no-default-features")]
92 pub no_default_features: bool,
93 #[serde(alias = "workspace")]
95 pub all: bool,
96 #[serde(deserialize_with = "humantime_serde", rename = "timeout")]
98 pub test_timeout: Duration,
99 pub release: bool,
101 #[serde(rename = "no-run")]
103 pub no_run: bool,
104 pub locked: bool,
106 pub frozen: bool,
108 pub target: Option<String>,
110 #[serde(rename = "target-dir")]
112 target_dir: Option<PathBuf>,
113 pub offline: bool,
115 pub command: Mode,
117 #[serde(rename = "run-types")]
119 pub run_types: Vec<RunType>,
120 pub packages: Vec<String>,
122 pub exclude: Vec<String>,
124 #[serde(skip_deserializing, skip_serializing)]
126 excluded_files: RefCell<Vec<glob::Pattern>>,
127 #[serde(rename = "exclude-files")]
129 excluded_files_raw: Vec<String>,
130 #[serde(skip_deserializing, skip_serializing)]
132 included_files: RefCell<Vec<glob::Pattern>>,
133 #[serde(rename = "include-files")]
135 included_files_raw: Vec<String>,
136 #[serde(rename = "args")]
138 pub varargs: Vec<String>,
139 pub features: Option<String>,
141 #[serde(rename = "Z")]
143 pub unstable_features: Vec<String>,
144 #[serde(rename = "out")]
146 pub generate: Vec<OutputFile>,
147 #[serde(rename = "test")]
149 pub test_names: HashSet<String>,
150 #[serde(rename = "bin")]
152 pub bin_names: HashSet<String>,
153 #[serde(rename = "example")]
155 pub example_names: HashSet<String>,
156 #[serde(rename = "bench")]
158 pub bench_names: HashSet<String>,
159 #[serde(rename = "no-fail-fast")]
161 pub no_fail_fast: bool,
162 pub profile: Option<String>,
164 #[serde(rename = "fail-under")]
166 pub fail_under: Option<f64>,
167 #[serde(skip_deserializing, skip_serializing)]
169 pub metadata: RefCell<Option<Metadata>>,
170 pub avoid_cfg_tarpaulin: bool,
172 pub color: Color,
174 #[serde(rename = "follow-exec")]
176 pub follow_exec: bool,
177 pub jobs: Option<usize>,
179 #[serde(rename = "implicit-test-threads")]
181 pub implicit_test_threads: bool,
182 engine: RefCell<TraceEngine>,
184 pub rustflags: Option<String>,
186 #[serde(rename = "include-tests")]
188 include_tests: bool,
189 #[serde(rename = "post-test-delay")]
190 pub post_test_delay: Option<Duration>,
192 objects: Vec<PathBuf>,
195 profraw_folder: PathBuf,
197 pub fail_immediately: bool,
199 pub stderr: bool,
201}
202
203fn default_test_timeout() -> Duration {
204 Duration::from_secs(60)
205}
206
207impl Default for Config {
208 fn default() -> Config {
209 Config {
210 name: String::new(),
211 command: Mode::Test,
212 run_types: vec![],
213 manifest: default_manifest(),
214 config: None,
215 root: Default::default(),
216 run_ignored: false,
217 include_tests: false,
218 ignore_panics: false,
219 force_clean: true,
220 skip_clean: false,
221 no_dead_code: false,
222 verbose: false,
223 debug: false,
224 follow_exec: false,
225 #[cfg(not(test))]
226 dump_traces: false,
227 #[cfg(test)]
228 dump_traces: true,
229 count: false,
230 line_coverage: true,
231 branch_coverage: false,
232 generate: vec![],
233 output_directory: Default::default(),
234 coveralls: None,
235 #[cfg(feature = "coveralls")]
236 ci_tool: None,
237 report_uri: None,
238 forward_signals: true,
239 no_default_features: false,
240 features: None,
241 unstable_features: vec![],
242 all: false,
243 packages: vec![],
244 exclude: vec![],
245 excluded_files: RefCell::new(vec![]),
246 excluded_files_raw: vec![],
247 included_files: RefCell::new(vec![]),
248 included_files_raw: vec![],
249 varargs: vec![],
250 test_timeout: default_test_timeout(),
251 release: false,
252 all_features: false,
253 no_run: false,
254 locked: false,
255 frozen: false,
256 implicit_test_threads: false,
257 target: None,
258 target_dir: None,
259 offline: false,
260 test_names: HashSet::new(),
261 example_names: HashSet::new(),
262 bin_names: HashSet::new(),
263 bench_names: HashSet::new(),
264 no_fail_fast: false,
265 profile: None,
266 fail_under: None,
267 metadata: RefCell::new(None),
268 avoid_cfg_tarpaulin: false,
269 jobs: None,
270 color: Color::Auto,
271 engine: RefCell::default(),
272 rustflags: None,
273 post_test_delay: Some(Duration::from_secs(1)),
274 objects: vec![],
275 profraw_folder: PathBuf::from("profraws"),
276 fail_immediately: false,
277 stderr: false,
278 }
279 }
280}
281
282impl From<ConfigArgs> for ConfigWrapper {
283 fn from(args: ConfigArgs) -> Self {
284 info!("Creating config");
285
286 let features = args.features;
287 let features = if features.is_empty() {
288 None
289 } else {
290 Some(features.join(" "))
291 };
292
293 let force_clean = match (args.force_clean, args.skip_clean) {
294 (true, false) | (false, false) => true,
295 (false, true) => false,
296 _ => {
297 warn!("skip-clean and force-clean are incompatible. Selecting force-clean");
298 true
299 }
300 };
301
302 let args_config = Config {
303 name: String::new(),
304 manifest: process_manifest(args.manifest_path, args.root.clone()),
305 config: None,
306 root: args.root,
307 engine: RefCell::new(args.engine.unwrap_or_default()),
308 command: args.command.unwrap_or(Mode::Test),
309 verbose: args.logging.verbose || args.logging.debug,
310 debug: args.logging.debug,
311 dump_traces: args.logging.debug || args.logging.dump_traces,
312 color: args.logging.color.unwrap_or(Color::Auto),
313 run_types: args.run_types.collect(),
314 run_ignored: args.ignored,
315 include_tests: args.include_tests,
316 ignore_panics: args.ignore_panics,
317 no_dead_code: args.no_dead_code,
318 force_clean,
319 skip_clean: !force_clean,
320 no_fail_fast: args.no_fail_fast,
321 follow_exec: args.follow_exec,
322 count: args.count,
323 line_coverage: args.line || !args.branch,
324 branch_coverage: args.branch || !args.line,
325 generate: args.out,
326 output_directory: args.output_dir,
327 coveralls: args.coveralls,
328 #[cfg(feature = "coveralls")]
329 ci_tool: args.ciserver.map(|c| c.0),
330 report_uri: args.report_uri,
331 forward_signals: true, all_features: args.all_features,
333 no_default_features: args.no_default_features,
334 features,
335 unstable_features: args.unstable_features,
336 all: args.all | args.workspace,
337 packages: args.packages,
338 exclude: args.exclude,
339 excluded_files_raw: args.exclude_files.iter().map(Pattern::to_string).collect(),
340 excluded_files: RefCell::new(args.exclude_files),
341 included_files_raw: args.include_files.iter().map(Pattern::to_string).collect(),
342 included_files: RefCell::new(args.include_files),
343 varargs: args.args,
344 test_timeout: Duration::from_secs(args.timeout.unwrap_or(60)),
345 release: args.release,
346 no_run: args.no_run,
347 locked: args.locked,
348 frozen: args.frozen,
349 target: args.target,
350 target_dir: process_target_dir(args.target_dir),
351 offline: args.offline,
352 test_names: args.test.into_iter().collect(),
353 bin_names: args.bin.into_iter().collect(),
354 bench_names: args.bench.into_iter().collect(),
355 example_names: args.example.into_iter().collect(),
356 fail_under: args.fail_under,
357 jobs: args.jobs,
358 profile: args.profile,
359 metadata: RefCell::new(None),
360 avoid_cfg_tarpaulin: args.avoid_cfg_tarpaulin,
361 implicit_test_threads: args.implicit_test_threads,
362 rustflags: args.rustflags,
363 post_test_delay: args.post_test_delay.map(Duration::from_secs),
364 objects: canonicalize_paths(args.objects),
365 profraw_folder: PathBuf::from("profraws"),
366 fail_immediately: args.fail_immediately,
367 stderr: args.logging.stderr,
368 };
369 if args.ignore_config {
370 Self(vec![args_config])
371 } else if let Some(mut path) = args.config {
372 if path.is_relative() {
373 path = env::current_dir()
374 .unwrap()
375 .join(path)
376 .canonicalize()
377 .unwrap();
378 }
379 let confs = Config::load_config_file(path);
380 Config::get_config_vec(confs, args_config)
381 } else if let Some(cfg) = args_config.check_for_configs() {
382 let confs = Config::load_config_file(cfg);
383 Config::get_config_vec(confs, args_config)
384 } else {
385 Self(vec![args_config])
386 }
387 }
388}
389
390impl Config {
391 pub fn engine(&self) -> TraceEngine {
394 let engine = *self.engine.borrow();
395 match engine {
396 TraceEngine::Auto | TraceEngine::Llvm if supports_llvm_coverage() => TraceEngine::Llvm,
397 engine => {
398 if engine == TraceEngine::Llvm {
399 error!("unable to utilise llvm coverage, due to compiler support. Falling back to Ptrace");
400 self.engine.replace(TraceEngine::Ptrace);
401 }
402 TraceEngine::Ptrace
403 }
404 }
405 }
406
407 pub fn set_engine(&self, engine: TraceEngine) {
408 self.engine.replace(engine);
409 }
410
411 pub fn set_clean(&mut self, clean: bool) {
412 self.force_clean = clean;
413 self.skip_clean = !clean;
414 }
415
416 pub fn set_include_tests(&mut self, include: bool) {
417 self.include_tests = include;
418 }
419
420 pub fn include_tests(&self) -> bool {
421 self.include_tests
422 }
423
424 pub fn force_clean(&self) -> bool {
425 self.force_clean && !self.skip_clean
428 }
429
430 pub fn target_dir(&self) -> PathBuf {
431 let res = if let Some(s) = &self.target_dir {
432 s.clone()
433 } else {
434 match *self.get_metadata() {
435 Some(ref meta) => PathBuf::from(meta.target_directory.clone()),
436 _ => self
437 .manifest
438 .parent()
439 .map(fix_unc_path)
440 .unwrap_or_default()
441 .join("target"),
442 }
443 };
444 fix_unc_path(&res)
445 }
446
447 pub fn profraw_dir(&self) -> PathBuf {
449 if self.profraw_folder.is_relative() {
450 self.target_dir()
451 .join("tarpaulin")
452 .join(&self.profraw_folder)
453 } else {
454 self.profraw_folder.clone()
455 }
456 }
457
458 pub fn set_profraw_folder(&mut self, path: PathBuf) {
461 self.profraw_folder = path;
462 }
463
464 pub fn set_target_dir(&mut self, target_dir: PathBuf) {
466 self.target_dir = Some(target_dir);
467 }
468
469 pub fn doctest_dir(&self) -> PathBuf {
470 let mut result = self.target_dir();
472 result.push("doctests");
473 result
474 }
475
476 pub(crate) fn get_metadata(&self) -> Ref<Option<Metadata>> {
477 if self.metadata.borrow().is_none() {
478 match MetadataCommand::new().manifest_path(&self.manifest).exec() {
479 Ok(meta) => {
480 self.metadata.replace(Some(meta));
481 }
482 Err(e) => warn!("Couldn't get project metadata {}", e),
483 }
484 }
485 self.metadata.borrow()
486 }
487
488 pub fn root(&self) -> PathBuf {
489 let res = match *self.get_metadata() {
490 Some(ref meta) => PathBuf::from(meta.workspace_root.clone()),
491 _ => self
492 .manifest
493 .parent()
494 .map(Path::to_path_buf)
495 .unwrap_or_default(),
496 };
497 fix_unc_path(&res)
498 }
499
500 pub fn manifest(&self) -> PathBuf {
501 fix_unc_path(&self.manifest)
502 }
503
504 pub fn set_manifest(&mut self, manifest: PathBuf) {
505 self.manifest = manifest;
506 }
507
508 pub fn output_dir(&self) -> PathBuf {
509 let path = if let Some(ref path) = self.output_directory {
510 if path.is_relative() {
511 self.root().join(path)
512 } else {
513 path.clone()
514 }
515 } else {
516 self.root()
517 };
518 fix_unc_path(&path)
519 }
520
521 pub fn get_config_vec(file_configs: std::io::Result<Vec<Self>>, backup: Self) -> ConfigWrapper {
522 if let Ok(mut confs) = file_configs {
523 for c in &mut confs {
524 c.merge(&backup);
525 }
526 if confs.is_empty() {
527 ConfigWrapper(vec![backup])
528 } else {
529 ConfigWrapper(confs)
530 }
531 } else {
532 warn!("Failed to deserialize config file falling back to provided args");
533 ConfigWrapper(vec![backup])
534 }
535 }
536
537 pub fn check_for_configs(&self) -> Option<PathBuf> {
539 if let Some(config_file) = env::var_os("CARGO_TARPAULIN_CONFIG_FILE") {
540 Some(config_file.into())
541 } else if let Some(root) = &self.root {
542 Self::check_path_for_configs(root)
543 } else if let Some(root) = self.manifest.clone().parent() {
544 Self::check_path_for_configs(root)
545 } else {
546 None
547 }
548 }
549
550 fn check_path_for_configs<P: AsRef<Path>>(path: P) -> Option<PathBuf> {
551 let mut path_1 = PathBuf::from(path.as_ref());
552 let mut path_2 = path_1.clone();
553 path_1.push("tarpaulin.toml");
554 path_2.push(".tarpaulin.toml");
555 if path_1.exists() {
556 Some(path_1)
557 } else if path_2.exists() {
558 Some(path_2)
559 } else {
560 None
561 }
562 }
563
564 pub fn load_config_file<P: AsRef<Path>>(file: P) -> std::io::Result<Vec<Self>> {
565 let buffer = fs::read_to_string(file.as_ref())?;
566 let mut res = Self::parse_config_toml(&buffer);
567 let parent = match file.as_ref().parent() {
568 Some(p) => p.to_path_buf(),
569 None => PathBuf::new(),
570 };
571 if let Ok(cfs) = res.as_mut() {
572 for c in cfs.iter_mut() {
573 c.config = Some(file.as_ref().to_path_buf());
574 c.manifest = make_absolute_with_parent(&c.manifest, &parent);
575 if let Some(root) = c.root.as_mut() {
576 *root = make_absolute_with_parent(&root, &parent);
577 }
578 if let Some(root) = c.output_directory.as_mut() {
579 *root = make_absolute_with_parent(&root, &parent);
580 }
581 if let Some(root) = c.target_dir.as_mut() {
582 *root = make_absolute_with_parent(&root, &parent);
583 }
584 }
585 }
586 res
587 }
588
589 pub fn parse_config_toml(buffer: &str) -> std::io::Result<Vec<Self>> {
590 let mut map: IndexMap<String, Self> = toml::from_str(buffer).map_err(|e| {
591 error!("Invalid config file {}", e);
592 Error::new(ErrorKind::InvalidData, format!("{e}"))
593 })?;
594
595 let mut result = Vec::new();
596 for (name, conf) in map.iter_mut() {
597 conf.name = name.to_string();
598 result.push(conf.clone());
599 }
600 if result.is_empty() {
601 Err(Error::new(ErrorKind::InvalidData, "No config tables"))
602 } else {
603 Ok(result)
604 }
605 }
606
607 pub fn merge(&mut self, other: &Config) {
610 if other.debug {
611 self.debug = other.debug;
612 self.verbose = other.verbose;
613 } else if other.verbose {
614 self.verbose = other.verbose;
615 }
616 self.no_run |= other.no_run;
617 self.no_default_features |= other.no_default_features;
618 self.ignore_panics |= other.ignore_panics;
619 self.forward_signals |= other.forward_signals;
621 self.run_ignored |= other.run_ignored;
622 self.release |= other.release;
623 self.no_dead_code |= other.no_dead_code;
624 self.count |= other.count;
625 self.all_features |= other.all_features;
626 self.implicit_test_threads |= other.implicit_test_threads;
627 self.line_coverage |= other.line_coverage;
628 self.branch_coverage |= other.branch_coverage;
629 self.dump_traces |= other.dump_traces;
630 self.offline |= other.offline;
631 self.stderr |= other.stderr;
632 if self.manifest != other.manifest && self.manifest == default_manifest() {
633 self.manifest = other.manifest.clone();
634 }
635 for obj in &other.objects {
636 if !self.objects.contains(obj) {
637 self.objects.push(obj.clone());
638 }
639 }
640 self.root = Config::pick_optional_config(&self.root, &other.root);
641 self.coveralls = Config::pick_optional_config(&self.coveralls, &other.coveralls);
642
643 cfg_if::cfg_if! {
644 if #[cfg(feature = "coveralls")] {
645 self.ci_tool = Config::pick_optional_config(&self.ci_tool, &other.ci_tool);
646 }
647 }
648
649 self.report_uri = Config::pick_optional_config(&self.report_uri, &other.report_uri);
650 self.target = Config::pick_optional_config(&self.target, &other.target);
651 self.target_dir = Config::pick_optional_config(&self.target_dir, &other.target_dir);
652 self.output_directory =
653 Config::pick_optional_config(&self.output_directory, &other.output_directory);
654 self.all |= other.all;
655 self.frozen |= other.frozen;
656 self.locked |= other.locked;
657 self.force_clean &= other.force_clean;
660 self.skip_clean |= other.skip_clean;
661 self.include_tests |= other.include_tests;
662 self.no_fail_fast |= other.no_fail_fast;
663
664 let end_delay = match (self.post_test_delay, other.post_test_delay) {
665 (Some(d), None) | (None, Some(d)) => Some(d),
666 (None, None) => None,
667 (Some(a), Some(b)) => Some(a.max(b)),
668 };
669 self.post_test_delay = end_delay;
670 match (self.force_clean, self.skip_clean) {
672 (true, false) | (false, true) => {}
673 (false, _) => {
674 self.skip_clean = true;
675 }
676 (_, true) => {
677 self.force_clean = false;
678 }
679 }
680
681 let new_flags = match (self.rustflags.as_ref(), other.rustflags.as_ref()) {
682 (Some(a), Some(b)) => Some(format!("{a} {b}")),
683 (Some(a), None) => Some(a.clone()),
684 (None, Some(b)) => Some(b.clone()),
685 _ => None,
686 };
687 self.rustflags = new_flags;
688
689 if self.jobs.is_none() {
690 self.jobs = other.jobs;
691 }
692 if self.fail_under.is_none()
693 || other.fail_under.is_some() && other.fail_under.unwrap() < self.fail_under.unwrap()
694 {
695 self.fail_under = other.fail_under;
696 }
697
698 if other.test_timeout != default_test_timeout() {
699 self.test_timeout = other.test_timeout;
700 }
701
702 if self.profile.is_none() && other.profile.is_some() {
703 self.profile = other.profile.clone();
704 }
705 if other.features.is_some() {
706 if self.features.is_none() {
707 self.features = other.features.clone();
708 } else if let Some(features) = self.features.as_mut() {
709 features.push(' ');
710 features.push_str(other.features.as_ref().unwrap());
711 }
712 }
713
714 let additional_packages = other
715 .packages
716 .iter()
717 .filter(|package| !self.packages.contains(package))
718 .cloned()
719 .collect::<Vec<String>>();
720 self.packages.extend(additional_packages);
721
722 let additional_outs = other
723 .generate
724 .iter()
725 .filter(|out| !self.generate.contains(out))
726 .copied()
727 .collect::<Vec<_>>();
728 self.generate.extend(additional_outs);
729
730 let additional_excludes = other
731 .exclude
732 .iter()
733 .filter(|package| !self.exclude.contains(package))
734 .cloned()
735 .collect::<Vec<String>>();
736 self.exclude.extend(additional_excludes);
737
738 let additional_varargs = other
739 .varargs
740 .iter()
741 .filter(|package| !self.varargs.contains(package))
742 .cloned()
743 .collect::<Vec<String>>();
744 self.varargs.extend(additional_varargs);
745
746 let additional_z_opts = other
747 .unstable_features
748 .iter()
749 .filter(|package| !self.unstable_features.contains(package))
750 .cloned()
751 .collect::<Vec<String>>();
752 self.unstable_features.extend(additional_z_opts);
753
754 let exclude = &self.exclude;
755 self.packages.retain(|package| {
756 let keep = !exclude.contains(package);
757 if !keep {
758 info!("{} is in exclude list removing from packages", package);
759 }
760 keep
761 });
762
763 for test in &other.test_names {
764 self.test_names.insert(test.clone());
765 }
766 for test in &other.bin_names {
767 self.bin_names.insert(test.clone());
768 }
769 for test in &other.example_names {
770 self.example_names.insert(test.clone());
771 }
772 for test in &other.bench_names {
773 self.bench_names.insert(test.clone());
774 }
775 for ty in &other.run_types {
776 if !self.run_types.contains(ty) {
777 self.run_types.push(*ty);
778 }
779 }
780
781 if !other.excluded_files_raw.is_empty() {
782 self.excluded_files_raw
783 .extend_from_slice(&other.excluded_files_raw);
784
785 let mut excluded_files = self.excluded_files.borrow_mut();
787 excluded_files.clear();
788 }
789
790 if !other.included_files_raw.is_empty() {
791 self.included_files_raw
792 .extend_from_slice(&other.included_files_raw);
793
794 let mut included_files = self.included_files.borrow_mut();
796 included_files.clear();
797 }
798 }
799
800 pub fn pick_optional_config<T: Clone>(
801 base_config: &Option<T>,
802 override_config: &Option<T>,
803 ) -> Option<T> {
804 if override_config.is_some() {
805 override_config.clone()
806 } else {
807 base_config.clone()
808 }
809 }
810
811 pub fn objects(&self) -> &[PathBuf] {
812 &self.objects
813 }
814
815 pub fn has_named_tests(&self) -> bool {
816 !(self.test_names.is_empty()
817 && self.bin_names.is_empty()
818 && self.example_names.is_empty()
819 && self.bench_names.is_empty())
820 }
821
822 #[inline]
823 pub fn is_coveralls(&self) -> bool {
824 self.coveralls.is_some()
825 }
826
827 #[inline]
828 pub fn exclude_path(&self, path: &Path) -> bool {
829 if self.excluded_files.borrow().len() != self.excluded_files_raw.len() {
830 let mut excluded_files = self.excluded_files.borrow_mut();
831 let mut compiled = globs_from_excluded(&self.excluded_files_raw);
832 excluded_files.clear();
833 excluded_files.append(&mut compiled);
834 }
835 let project = self.strip_base_dir(path);
836
837 self.excluded_files
838 .borrow()
839 .iter()
840 .any(|x| x.matches_path(&project))
841 }
842
843 #[inline]
844 pub fn include_path(&self, path: &Path) -> bool {
845 if self.included_files.borrow().len() != self.included_files_raw.len() {
846 let mut included_files = self.included_files.borrow_mut();
847 let mut compiled = globs_from_excluded(&self.included_files_raw);
848 included_files.clear();
849 included_files.append(&mut compiled);
850 }
851
852 let project = self.strip_base_dir(path);
853
854 if self.included_files.borrow().is_empty() {
856 return true;
857 }
858
859 self.included_files
860 .borrow()
861 .iter()
862 .any(|x| x.matches_path(&project))
863 }
864
865 #[inline]
868 pub fn get_base_dir(&self) -> PathBuf {
869 let root = self.root();
870 let res = if root.is_absolute() {
871 root
872 } else {
873 let base_dir = env::current_dir().unwrap();
874 if let Ok(res) = base_dir.join(root).canonicalize() {
875 res
876 } else {
877 base_dir
878 }
879 };
880 fix_unc_path(&res)
881 }
882
883 #[inline]
885 pub fn strip_base_dir(&self, path: &Path) -> PathBuf {
886 path_relative_from(path, &self.get_base_dir()).unwrap_or_else(|| path.to_path_buf())
887 }
888
889 #[inline]
890 pub fn is_default_output_dir(&self) -> bool {
891 self.output_directory.is_none()
892 }
893}
894
895fn make_absolute_with_parent(path: impl AsRef<Path>, parent: impl AsRef<Path>) -> PathBuf {
896 let path = path.as_ref();
897 if path.is_relative() {
898 parent.as_ref().join(path)
899 } else {
900 path.to_path_buf()
901 }
902}
903
904pub fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
909 use std::path::Component;
910
911 if path.is_absolute() != base.is_absolute() {
912 if path.is_absolute() {
913 Some(path.to_path_buf())
914 } else {
915 None
916 }
917 } else {
918 let mut ita = path.components();
919 let mut itb = base.components();
920 let mut comps = vec![];
921
922 loop {
923 match (ita.next(), itb.next()) {
924 (None, None) => break,
925 (Some(a), None) => {
926 comps.push(a);
927 comps.extend(ita.by_ref());
928 break;
929 }
930 (None, _) => comps.push(Component::ParentDir),
931 (Some(a), Some(b)) if comps.is_empty() && a == b => (),
932 (Some(a), Some(Component::CurDir)) => comps.push(a),
933 (Some(_), Some(Component::ParentDir)) => return None,
934 (Some(a), Some(_)) => {
935 comps.push(Component::ParentDir);
936 for _ in itb {
937 comps.push(Component::ParentDir);
938 }
939 comps.push(a);
940 comps.extend(ita.by_ref());
941 break;
942 }
943 }
944 }
945 Some(comps.iter().map(|c| c.as_os_str()).collect())
946 }
947}
948
949#[cfg(test)]
950mod tests {
951 use clap::Parser;
952
953 use crate::args::TarpaulinCli;
954
955 use super::*;
956
957 #[test]
958 fn is_root_absolute() {
959 let args = TarpaulinCli::parse_from(vec!["tarpaulin", "-r", "."]);
960 let conf = ConfigWrapper::from(args.config).0;
961 assert!(conf[0].root().is_absolute());
962 }
963
964 #[test]
965 fn features_args() {
966 let args = TarpaulinCli::parse_from(vec![
967 "tarpaulin",
968 "--ignore-config",
969 "--features",
970 "a",
971 "--features",
972 "b",
973 ]);
974 let conf = ConfigWrapper::from(args.config).0;
975 assert_eq!(conf.len(), 1);
976 assert_eq!(conf[0].features, Some("a b".to_string()));
977
978 let args =
979 TarpaulinCli::parse_from(vec!["tarpaulin", "--ignore-config", "--features", "a b"]);
980 let conf = ConfigWrapper::from(args.config).0;
981 assert_eq!(conf.len(), 1);
982 assert_eq!(conf[0].features, Some("a b".to_string()));
983 }
984
985 #[test]
986 fn exclude_paths() {
987 let args = TarpaulinCli::parse_from(vec!["tarpaulin", "--exclude-files", "*module*"]);
988 let conf = ConfigWrapper::from(args.config).0;
989 assert_eq!(conf.len(), 1);
990 assert!(conf[0].exclude_path(Path::new("src/module/file.rs")));
991 assert!(!conf[0].exclude_path(Path::new("src/mod.rs")));
992 assert!(!conf[0].exclude_path(Path::new("unrelated.rs")));
993 assert!(conf[0].exclude_path(Path::new("module.rs")));
994 }
995
996 #[test]
997 fn exclude_paths_directory_separators() {
998 let args = TarpaulinCli::parse_from(vec![
999 "tarpaulin",
1000 "--exclude-files",
1001 "src/foo/*",
1002 "src\\bar\\*",
1003 ]);
1004 let conf = ConfigWrapper::from(args.config).0;
1005 assert_eq!(conf.len(), 1);
1006 assert!(conf[0].exclude_path(Path::new("src/foo/file.rs")));
1007 assert!(conf[0].exclude_path(Path::new("src\\bar\\file.rs")));
1008
1009 cfg_if::cfg_if! {
1010 if #[cfg(windows)] {
1011 assert!(conf[0].exclude_path(Path::new("src\\foo\\file.rs")));
1012 assert!(conf[0].exclude_path(Path::new("src/bar/file.rs")));
1013 } else {
1014 assert!(!conf[0].exclude_path(Path::new("src\\foo\\file.rs")));
1015 assert!(!conf[0].exclude_path(Path::new("src/bar/file.rs")));
1016 }
1017 }
1018 }
1019
1020 #[test]
1021 fn include_paths_directory_separators() {
1022 let args = TarpaulinCli::parse_from(vec![
1023 "tarpaulin",
1024 "--include-files",
1025 "src/foo/*",
1026 "src\\bar\\*",
1027 ]);
1028 let conf = ConfigWrapper::from(args.config).0;
1029 assert_eq!(conf.len(), 1);
1030 assert!(conf[0].include_path(Path::new("src/foo/file.rs")));
1031 assert!(conf[0].include_path(Path::new("src\\bar\\file.rs")));
1032
1033 cfg_if::cfg_if! {
1034 if #[cfg(windows)] {
1035 assert!(conf[0].include_path(Path::new("src\\foo\\file.rs")));
1036 assert!(conf[0].include_path(Path::new("src/bar/file.rs")));
1037 } else {
1038 assert!(!conf[0].include_path(Path::new("src\\foo\\file.rs")));
1039 assert!(!conf[0].include_path(Path::new("src/bar/file.rs")));
1040 }
1041 }
1042 }
1043
1044 #[test]
1045 fn no_exclusions() {
1046 let args = TarpaulinCli::parse_from(vec!["tarpaulin"]);
1047 let conf = ConfigWrapper::from(args.config).0;
1048 assert_eq!(conf.len(), 1);
1049 assert!(!conf[0].exclude_path(Path::new("src/module/file.rs")));
1050 assert!(!conf[0].exclude_path(Path::new("src/mod.rs")));
1051 assert!(!conf[0].exclude_path(Path::new("unrelated.rs")));
1052 assert!(!conf[0].exclude_path(Path::new("module.rs")));
1053
1054 assert!(conf[0].include_path(Path::new("src/module/file.rs")));
1055 assert!(conf[0].include_path(Path::new("src/mod.rs")));
1056 assert!(conf[0].include_path(Path::new("unrelated.rs")));
1057 assert!(conf[0].include_path(Path::new("module.rs")));
1058 }
1059
1060 #[test]
1061 fn exclude_exact_file() {
1062 let args = TarpaulinCli::parse_from(vec!["tarpaulin", "--exclude-files", "*/lib.rs"]);
1063 let conf = ConfigWrapper::from(args.config).0;
1064 assert_eq!(conf.len(), 1);
1065 assert!(conf[0].exclude_path(Path::new("src/lib.rs")));
1066 assert!(!conf[0].exclude_path(Path::new("src/mod.rs")));
1067 assert!(!conf[0].exclude_path(Path::new("src/notlib.rs")));
1068 assert!(!conf[0].exclude_path(Path::new("lib.rs")));
1069 }
1070
1071 #[test]
1072 fn include_exact_file() {
1073 let args = TarpaulinCli::parse_from(vec!["tarpaulin", "--include-files", "*/lib.rs"]);
1074 let conf = ConfigWrapper::from(args.config).0;
1075 assert_eq!(conf.len(), 1);
1076 assert!(conf[0].include_path(Path::new("src/lib.rs")));
1077 assert!(!conf[0].include_path(Path::new("src/mod.rs")));
1078 assert!(!conf[0].include_path(Path::new("src/notlib.rs")));
1079 assert!(!conf[0].include_path(Path::new("lib.rs")));
1080 }
1081
1082 #[test]
1083 fn relative_path_test() {
1084 cfg_if::cfg_if! {
1085 if #[cfg(windows)] {
1086 let root_base = "C:";
1087 } else {
1088 let root_base = "";
1089 }
1090 }
1091 let path_a = PathBuf::from(format!("{root_base}/this/should/form/a/rel/path/"));
1092 let path_b = PathBuf::from(format!("{root_base}/this/should/form/b/rel/path/"));
1093
1094 let rel_path = path_relative_from(&path_b, &path_a);
1095 assert!(rel_path.is_some());
1096 assert_eq!(
1097 rel_path.unwrap(),
1098 Path::new("../../../b/rel/path"),
1099 "Wrong relative path"
1100 );
1101
1102 let path_a = PathBuf::from(format!("{root_base}/this/should/not/form/a/rel/path/"));
1103 let path_b = Path::new("this/should/not/form/a/rel/path/");
1104 assert!(!path_b.is_absolute());
1105 assert!(path_a.is_absolute());
1106 let rel_path = path_relative_from(path_b, &path_a);
1107 assert_eq!(rel_path, None, "Did not expect relative path");
1108
1109 let path_a = Path::new("this/should/form/a/rel/path/");
1110 let path_b = Path::new("this/should/form/b/rel/path/");
1111
1112 let rel_path = path_relative_from(path_b, path_a);
1113 assert!(rel_path.is_some());
1114 assert_eq!(
1115 rel_path.unwrap(),
1116 Path::new("../../../b/rel/path"),
1117 "Wrong relative path"
1118 );
1119 }
1120
1121 #[test]
1122 fn config_toml() {
1123 let toml = "[global]
1124 ignored= true
1125 coveralls= \"hello\"
1126
1127 [other]
1128 run-types = [\"Doctests\", \"Tests\"]";
1129
1130 let configs = Config::parse_config_toml(toml).unwrap();
1131 assert_eq!(configs.len(), 2);
1132 for c in &configs {
1133 if c.name == "global" {
1134 assert!(c.run_ignored);
1135 assert_eq!(c.coveralls, Some("hello".to_string()));
1136 } else if c.name == "other" {
1137 assert_eq!(c.run_types, vec![RunType::Doctests, RunType::Tests]);
1138 } else {
1139 panic!("Unexpected name {}", c.name);
1140 }
1141 }
1142 }
1143
1144 #[test]
1145 fn excluded_merge() {
1146 let toml = r#"[a]
1147 exclude-files = ["target/*"]
1148 [b]
1149 exclude-files = ["foo.rs"]
1150 "#;
1151
1152 let mut configs = Config::parse_config_toml(toml).unwrap();
1153 let mut config = configs.remove(0);
1154 config.merge(&configs[0]);
1155 assert!(config.excluded_files_raw.contains(&"target/*".to_string()));
1156 assert!(config.excluded_files_raw.contains(&"foo.rs".to_string()));
1157
1158 assert_eq!(config.excluded_files_raw.len(), 2);
1159 assert_eq!(configs[0].excluded_files_raw.len(), 1);
1160 }
1161
1162 #[test]
1163 fn target_merge() {
1164 let toml_a = r#""#;
1165 let toml_b = r#"target = "wasm32-unknown-unknown""#;
1166 let toml_c = r#"target = "x86_64-linux-gnu""#;
1167
1168 let mut a: Config = toml::from_str(toml_a).unwrap();
1169 let mut b: Config = toml::from_str(toml_b).unwrap();
1170 let c: Config = toml::from_str(toml_c).unwrap();
1171
1172 assert_eq!(a.target, None);
1173 assert_eq!(b.target, Some(String::from("wasm32-unknown-unknown")));
1174 assert_eq!(c.target, Some(String::from("x86_64-linux-gnu")));
1175
1176 b.merge(&c);
1177 assert_eq!(b.target, Some(String::from("x86_64-linux-gnu")));
1178
1179 a.merge(&b);
1180 assert_eq!(a.target, Some(String::from("x86_64-linux-gnu")));
1181 }
1182
1183 #[test]
1184 fn workspace_merge() {
1185 let toml_a = r#"workspace = false"#;
1186 let toml_b = r#"workspace = true"#;
1187
1188 let mut a: Config = toml::from_str(toml_a).unwrap();
1189 let b: Config = toml::from_str(toml_b).unwrap();
1190
1191 assert!(!a.all);
1192 assert!(b.all);
1193
1194 a.merge(&b);
1195 assert!(a.all);
1196 }
1197
1198 #[test]
1199 fn packages_merge() {
1200 let toml_a = r#"packages = []"#;
1201 let toml_b = r#"packages = ["a"]"#;
1202 let toml_c = r#"packages = ["b", "a"]"#;
1203
1204 let mut a: Config = toml::from_str(toml_a).unwrap();
1205 let mut b: Config = toml::from_str(toml_b).unwrap();
1206 let c: Config = toml::from_str(toml_c).unwrap();
1207
1208 assert_eq!(a.packages, Vec::<String>::new());
1209 assert_eq!(b.packages, vec![String::from("a")]);
1210 assert_eq!(c.packages, vec![String::from("b"), String::from("a")]);
1211
1212 a.merge(&c);
1213 assert_eq!(a.packages, vec![String::from("b"), String::from("a")]);
1214
1215 b.merge(&c);
1216 assert_eq!(b.packages, vec![String::from("a"), String::from("b")]);
1217 }
1218
1219 #[test]
1220 fn exclude_packages_merge() {
1221 let toml_a = r#"packages = []
1222 exclude = ["a"]"#;
1223 let toml_b = r#"packages = ["a"]
1224 exclude = ["b"]"#;
1225 let toml_c = r#"packages = ["b", "a"]
1226 exclude = ["c"]"#;
1227
1228 let mut a: Config = toml::from_str(toml_a).unwrap();
1229 let mut b: Config = toml::from_str(toml_b).unwrap();
1230 let c: Config = toml::from_str(toml_c).unwrap();
1231
1232 assert_eq!(a.exclude, vec![String::from("a")]);
1233 assert_eq!(b.exclude, vec![String::from("b")]);
1234 assert_eq!(c.exclude, vec![String::from("c")]);
1235
1236 a.merge(&c);
1237 assert_eq!(a.packages, vec![String::from("b")]);
1238 assert_eq!(a.exclude, vec![String::from("a"), String::from("c")]);
1239
1240 b.merge(&c);
1241 assert_eq!(b.packages, vec![String::from("a")]);
1242 assert_eq!(b.exclude, vec![String::from("b"), String::from("c")]);
1243 }
1244
1245 #[cfg(feature = "coveralls")]
1246 #[test]
1247 fn coveralls_merge() {
1248 let toml = r#"[a]
1249 coveralls = "abcd"
1250 report-uri = "https://example.com/report"
1251
1252 [b]
1253 coveralls = "xyz"
1254 ciserver = "coveralls-ruby"
1255 "#;
1256
1257 let configs = Config::parse_config_toml(toml).unwrap();
1258 let mut a_config = configs.iter().find(|x| x.name == "a").unwrap().clone();
1259 let b_config = configs.iter().find(|x| x.name == "b").unwrap();
1260 a_config.merge(b_config);
1261 assert_eq!(a_config.coveralls, Some("xyz".to_string()));
1262 assert_eq!(
1263 a_config.ci_tool,
1264 Some(CiService::Other("coveralls-ruby".to_string()))
1265 );
1266 assert_eq!(
1267 a_config.report_uri,
1268 Some("https://example.com/report".to_string())
1269 );
1270 }
1271
1272 #[test]
1273 fn output_dir_merge() {
1274 cfg_if::cfg_if! {
1275 if #[cfg(windows)] {
1276 let toml = r#"[has_dir]
1277 output-dir = "C:/foo"
1278
1279 [no_dir]
1280 coveralls = "xyz"
1281
1282 [other_dir]
1283 output-dir = "C:/bar"
1284 "#;
1285 let foo_dir = PathBuf::from("C:/foo");
1286 let bar_dir = PathBuf::from("C:/bar");
1287 } else {
1288 let toml = r#"[has_dir]
1289 output-dir = "/foo"
1290
1291 [no_dir]
1292 coveralls = "xyz"
1293
1294 [other_dir]
1295 output-dir = "/bar"
1296 "#;
1297 let foo_dir = PathBuf::from("/foo");
1298 let bar_dir = PathBuf::from("/bar");
1299 }
1300 }
1301
1302 let configs = Config::parse_config_toml(toml).unwrap();
1303 let has_dir = configs
1304 .iter()
1305 .find(|x| x.name == "has_dir")
1306 .unwrap()
1307 .clone();
1308 let no_dir = configs.iter().find(|x| x.name == "no_dir").unwrap().clone();
1309 let other_dir = configs
1310 .iter()
1311 .find(|x| x.name == "other_dir")
1312 .unwrap()
1313 .clone();
1314
1315 let mut merged_into_has_dir = has_dir.clone();
1316 merged_into_has_dir.merge(&no_dir);
1317 assert_eq!(merged_into_has_dir.output_dir(), foo_dir);
1318
1319 let mut merged_into_no_dir = no_dir.clone();
1320 merged_into_no_dir.merge(&has_dir);
1321 assert_eq!(merged_into_no_dir.output_dir(), foo_dir);
1322
1323 let mut neither_merged_dir = no_dir.clone();
1324 neither_merged_dir.merge(&no_dir);
1325 assert_eq!(neither_merged_dir.output_dir(), env::current_dir().unwrap());
1326
1327 let mut both_merged_dir = has_dir;
1328 both_merged_dir.merge(&other_dir);
1329 assert_eq!(both_merged_dir.output_dir(), bar_dir);
1330 }
1331
1332 #[test]
1333 fn rustflags_merge() {
1334 let toml = r#"
1335 [flag1]
1336 rustflags = "xyz"
1337
1338 [flag2]
1339 rustflags = "bar"
1340 "#;
1341
1342 let configs = Config::parse_config_toml(toml).unwrap();
1343 let flag1 = configs.iter().find(|x| x.name == "flag1").unwrap().clone();
1344 let flag2 = configs.iter().find(|x| x.name == "flag2").unwrap().clone();
1345 let noflags = Config::default();
1346
1347 let mut yes_no = flag1.clone();
1348 yes_no.merge(&noflags);
1349 assert_eq!(yes_no.rustflags, Some("xyz".to_string()));
1350
1351 let mut no_yes = noflags.clone();
1352 no_yes.merge(&flag2);
1353 assert_eq!(no_yes.rustflags, Some("bar".to_string()));
1354
1355 let mut f1_2 = flag1;
1356 f1_2.merge(&flag2);
1357 let flags = f1_2.rustflags.unwrap();
1358 let split = flags.split_ascii_whitespace().collect::<Vec<_>>();
1359 assert_eq!(split.len(), 2);
1360 assert!(split.contains(&"xyz"));
1361 assert!(split.contains(&"bar"));
1362 }
1363
1364 #[test]
1365 fn all_toml_options() {
1366 let toml = r#"[all]
1367 debug = true
1368 verbose = true
1369 ignore-panics = true
1370 count = true
1371 ignored = true
1372 force-clean = true
1373 branch = true
1374 forward = true
1375 coveralls = "hello"
1376 report-uri = "http://hello.com"
1377 no-default-features = true
1378 features = "a b"
1379 all-features = true
1380 workspace = true
1381 packages = ["pack_1"]
1382 exclude = ["pack_2"]
1383 exclude-files = ["fuzz/*"]
1384 timeout = "5s"
1385 release = true
1386 no-run = true
1387 locked = true
1388 frozen = true
1389 target = "wasm32-unknown-unknown"
1390 target-dir = "/tmp"
1391 offline = true
1392 Z = ["something-nightly"]
1393 out = ["Html"]
1394 run-types = ["Doctests"]
1395 root = "/home/rust"
1396 manifest-path = "/home/rust/foo/Cargo.toml"
1397 ciserver = "travis-ci"
1398 args = ["--nocapture"]
1399 test = ["test1", "test2"]
1400 bin = ["bin"]
1401 example = ["example"]
1402 bench = ["bench"]
1403 no-fail-fast = true
1404 profile = "Release"
1405 dump-traces = true
1406 all-targets = true
1407 "#;
1408 let mut configs = Config::parse_config_toml(toml).unwrap();
1409 assert_eq!(configs.len(), 1);
1410 let config = configs.remove(0);
1411 assert!(config.debug);
1412 assert!(config.verbose);
1413 assert!(config.dump_traces);
1414 assert!(config.ignore_panics);
1415 assert!(config.count);
1416 assert!(config.run_ignored);
1417 assert!(config.force_clean);
1418 assert!(config.branch_coverage);
1419 assert!(config.forward_signals);
1420 assert_eq!(config.coveralls, Some("hello".to_string()));
1421 assert_eq!(config.report_uri, Some("http://hello.com".to_string()));
1422 assert!(config.no_default_features);
1423 assert!(config.all_features);
1424 assert!(config.all);
1425 assert!(config.release);
1426 assert!(config.no_run);
1427 assert!(config.locked);
1428 assert!(config.frozen);
1429 assert_eq!(Some(String::from("wasm32-unknown-unknown")), config.target);
1430 assert_eq!(Some(Path::new("/tmp").to_path_buf()), config.target_dir);
1431 assert!(config.offline);
1432 assert_eq!(config.test_timeout, Duration::from_secs(5));
1433 assert_eq!(config.unstable_features.len(), 1);
1434 assert_eq!(config.unstable_features[0], "something-nightly");
1435 assert_eq!(config.varargs.len(), 1);
1436 assert_eq!(config.varargs[0], "--nocapture");
1437 assert_eq!(config.features, Some(String::from("a b")));
1438 assert_eq!(config.excluded_files_raw.len(), 1);
1439 assert_eq!(config.excluded_files_raw[0], "fuzz/*");
1440 assert_eq!(config.packages.len(), 1);
1441 assert_eq!(config.packages[0], "pack_1");
1442 assert_eq!(config.exclude.len(), 1);
1443 assert_eq!(config.exclude[0], "pack_2");
1444 assert_eq!(config.generate.len(), 1);
1445 assert_eq!(config.generate[0], OutputFile::Html);
1446 assert_eq!(config.run_types.len(), 1);
1447 assert_eq!(config.run_types[0], RunType::Doctests);
1448 assert_eq!(config.ci_tool, Some(CiService::Travis));
1449 assert_eq!(config.root, Some("/home/rust".into()));
1450 assert_eq!(config.manifest, PathBuf::from("/home/rust/foo/Cargo.toml"));
1451 assert_eq!(config.profile, Some("Release".to_string()));
1452 assert!(config.no_fail_fast);
1453 assert!(config.test_names.contains("test1"));
1454 assert!(config.test_names.contains("test2"));
1455 assert!(config.bin_names.contains("bin"));
1456 assert!(config.example_names.contains("example"));
1457 assert!(config.bench_names.contains("bench"));
1458 }
1459}