Repository: xd009642/tarpaulin
Pull request #1667 (2024-12-26 21:23:55)

Added feature --include-files
Added the flag `--include-files` to tarpaulin to only have a select amount of files that we get coverage from. It can save some time, as we are not looking at the traces of excluded files.

A condition has been added in the include_path function to make sure that --include-files is deactivated when no argument has been passed: otherwise, this would exclude all files from the report (as --include-files excludes all files by default).

Pretty much copied from the `--exclude-files` feature. Added some tests to make sure everything was working fine.

Here are some commands that I tested on another Rust project from the built image:
cargo tarpaulin --include-files src/jwt/mod.rs --out xml --> SUCCESS only 5 lines seen, 60% coverage
cargo tarpaulin --include-files src/jwt/* --exclude-files src/utils/* --out xml --> SUCCESS only 5 lines seen, 60% coverage.
cargo tarpaulin --exclude-files src/utils/* --out xml --> SUCCESS 101 lines seen, 3% coverage


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5a3fac3..02952d2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -537,9 +537,9 @@ dependencies = [
 
 [[package]]
 name = "glob"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
 
 [[package]]
 name = "gzip-header"
@@ -1167,18 +1167,18 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
 
 [[package]]
 name = "quick-xml"
-version = "0.37.1"
+version = "0.37.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03"
+checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
 dependencies = [
  "memchr",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.37"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
 dependencies = [
  "proc-macro2",
 ]
@@ -1390,18 +1390,18 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.216"
+version = "1.0.217"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
+checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.216"
+version = "1.0.217"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index 6274f07..0ea5fc4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -53,7 +53,7 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features =
     "tracing-log",
 ] }
 walkdir = "2.5.0"
-glob = "0.3.1"
+glob = "0.3.2"
 
 [target.'cfg(all(target_os = "linux", any(target_arch = "x86_64", target_arch = "x86")))'.dependencies]
 libc = "0.2.169"
diff --git a/README.md b/README.md
index 401ccd6..bb5add9 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ Options:
   -p, --packages [<PACKAGE>...]    Package id specifications for which package should be build. See cargo help pkgid for more info
   -e, --exclude [<PACKAGE>...]     Package id specifications to exclude from coverage. See cargo help pkgid for more info
       --exclude-files [<FILE>...]  Exclude given files from coverage results has * wildcard
+      --include-files [<FILE>...]  Include only given files in coverage results. Can have a * wildcard
   -t, --timeout <SECONDS>          Integer for the maximum time in seconds without response from test before timeout (default is 1 minute)
       --post-test-delay <SECONDS>  Delay after test to collect coverage profiles
       --follow-exec                Follow executed processes capturing coverage information if they're part of your project
diff --git a/src/args.rs b/src/args.rs
index 03b438d..aaf280c 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -134,6 +134,9 @@ pub struct ConfigArgs {
     /// Exclude given files from coverage results has * wildcard
     #[arg(long, value_name = "FILE", num_args = 0..)]
     pub exclude_files: Vec<Pattern>,
+    /// Include only given files in coverage results. Can have a * wildcard
+    #[arg(long, value_name = "FILE", num_args = 0..)]
+    pub include_files: Vec<Pattern>,
     /// Integer for the maximum time in seconds without response from test before timeout (default is 1 minute).
     #[arg(long, short, value_name = "SECONDS")]
     pub timeout: Option<u64>,
diff --git a/src/config/mod.rs b/src/config/mod.rs
index f04d870..025db54 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -127,6 +127,12 @@ pub struct Config {
     /// Files to exclude from testing in uncompiled form (for serde)
     #[serde(rename = "exclude-files")]
     excluded_files_raw: Vec<String>,
+    /// Files to include in testing in their compiled form
+    #[serde(skip_deserializing, skip_serializing)]
+    included_files: RefCell<Vec<glob::Pattern>>,
+    /// Files to include in testing in uncompiled form (for serde)
+    #[serde(rename = "include-files")]
+    included_files_raw: Vec<String>,
     /// Varargs to be forwarded to the test executables.
     #[serde(rename = "args")]
     pub varargs: Vec<String>,
@@ -238,6 +244,8 @@ impl Default for Config {
             exclude: vec![],
             excluded_files: RefCell::new(vec![]),
             excluded_files_raw: vec![],
+            included_files: RefCell::new(vec![]),
+            included_files_raw: vec![],
             varargs: vec![],
             test_timeout: default_test_timeout(),
             release: false,
@@ -330,6 +338,8 @@ impl From<ConfigArgs> for ConfigWrapper {
             exclude: args.exclude,
             excluded_files_raw: args.exclude_files.iter().map(Pattern::to_string).collect(),
             excluded_files: RefCell::new(args.exclude_files),
+            included_files_raw: args.include_files.iter().map(Pattern::to_string).collect(),
+            included_files: RefCell::new(args.include_files),
             varargs: args.args,
             test_timeout: Duration::from_secs(args.timeout.unwrap_or(60)),
             release: args.release,
@@ -776,6 +786,15 @@ impl Config {
             let mut excluded_files = self.excluded_files.borrow_mut();
             excluded_files.clear();
         }
+
+        if !other.included_files_raw.is_empty() {
+            self.included_files_raw
+                .extend_from_slice(&other.included_files_raw);
+
+            // Now invalidated the compiled regex cache so clear it
+            let mut included_files = self.included_files.borrow_mut();
+            included_files.clear();
+        }
     }
 
     pub fn pick_optional_config<T: Clone>(
@@ -821,6 +840,28 @@ impl Config {
             .any(|x| x.matches_path(&project))
     }
 
+    #[inline]
+    pub fn include_path(&self, path: &Path) -> bool {
+        if self.included_files.borrow().len() != self.included_files_raw.len() {
+            let mut included_files = self.included_files.borrow_mut();
+            let mut compiled = globs_from_excluded(&self.included_files_raw);
+            included_files.clear();
+            included_files.append(&mut compiled);
+        }
+
+        let project = self.strip_base_dir(path);
+
+        //if empty, then parameter not used, thus all files are included by default
+        if self.included_files.borrow().is_empty() {
+            return true;
+        }
+
+        self.included_files
+            .borrow()
+            .iter()
+            .any(|x| x.matches_path(&project))
+    }
+
     /// returns the relative path from the base_dir
     /// uses root if set, else env::current_dir()
     #[inline]
@@ -976,6 +1017,30 @@ mod tests {
         }
     }
 
+    #[test]
+    fn include_paths_directory_separators() {
+        let args = TarpaulinCli::parse_from(vec![
+            "tarpaulin",
+            "--include-files",
+            "src/foo/*",
+            "src\\bar\\*",
+        ]);
+        let conf = ConfigWrapper::from(args.config).0;
+        assert_eq!(conf.len(), 1);
+        assert!(conf[0].include_path(Path::new("src/foo/file.rs")));
+        assert!(conf[0].include_path(Path::new("src\\bar\\file.rs")));
+
+        cfg_if::cfg_if! {
+            if #[cfg(windows)] {
+                assert!(conf[0].include_path(Path::new("src\\foo\\file.rs")));
+                assert!(conf[0].include_path(Path::new("src/bar/file.rs")));
+            } else {
+                assert!(!conf[0].include_path(Path::new("src\\foo\\file.rs")));
+                assert!(!conf[0].include_path(Path::new("src/bar/file.rs")));
+            }
+        }
+    }
+
     #[test]
     fn no_exclusions() {
         let args = TarpaulinCli::parse_from(vec!["tarpaulin"]);
@@ -985,6 +1050,11 @@ mod tests {
         assert!(!conf[0].exclude_path(Path::new("src/mod.rs")));
         assert!(!conf[0].exclude_path(Path::new("unrelated.rs")));
         assert!(!conf[0].exclude_path(Path::new("module.rs")));
+
+        assert!(conf[0].include_path(Path::new("src/module/file.rs")));
+        assert!(conf[0].include_path(Path::new("src/mod.rs")));
+        assert!(conf[0].include_path(Path::new("unrelated.rs")));
+        assert!(conf[0].include_path(Path::new("module.rs")));
     }
 
     #[test]
@@ -998,6 +1068,17 @@ mod tests {
         assert!(!conf[0].exclude_path(Path::new("lib.rs")));
     }
 
+    #[test]
+    fn include_exact_file() {
+        let args = TarpaulinCli::parse_from(vec!["tarpaulin", "--include-files", "*/lib.rs"]);
+        let conf = ConfigWrapper::from(args.config).0;
+        assert_eq!(conf.len(), 1);
+        assert!(conf[0].include_path(Path::new("src/lib.rs")));
+        assert!(!conf[0].include_path(Path::new("src/mod.rs")));
+        assert!(!conf[0].include_path(Path::new("src/notlib.rs")));
+        assert!(!conf[0].include_path(Path::new("lib.rs")));
+    }
+
     #[test]
     fn relative_path_test() {
         cfg_if::cfg_if! {
diff --git a/src/path_utils.rs b/src/path_utils.rs
index d2596cb..3c1ed77 100644
--- a/src/path_utils.rs
+++ b/src/path_utils.rs
@@ -94,6 +94,7 @@ pub fn get_source_walker(config: &Config) -> impl Iterator<Item = DirEntry> + '_
         .filter_entry(move |e| is_coverable_file_path(e.path(), &root, &target))
         .filter_map(Result::ok)
         .filter(move |e| !(config.exclude_path(e.path())))
+        .filter(move |e| config.include_path(e.path()))
         .filter(is_source_file)
 }
 
diff --git a/tarpaulin.toml b/tarpaulin.toml
index fc91b1a..f7feec5 100644
--- a/tarpaulin.toml
+++ b/tarpaulin.toml
@@ -113,6 +113,9 @@ exclude = []
 # List of file paths to exclude from testing.
 exclude-files = []
 
+# List of file paths to include (and exclude all others) from testing.
+include-files = []
+
 # Additional arguments to pass to the test executables.
 args = []
 


Test patch:
diff --git a/src/test_loader.rs b/src/test_loader.rs
index 5df3add..1a6cc48 100644
--- a/src/test_loader.rs
+++ b/src/test_loader.rs
@@ -287,6 +287,7 @@ fn get_line_addresses<'data>(
                         config.include_tests() || !k.path.starts_with(project.join("tests"))
                     })
                     .filter(|(ref k, _)| !(config.exclude_path(&k.path)))
+                    .filter(|(ref k, _)| config.include_path(&k.path))
                     .filter(|(ref k, _)| {
                         !analysis.should_ignore(k.path.as_ref(), &(k.line as usize))
                     })
@@ -335,25 +336,34 @@ fn get_line_addresses<'data>(
         }
     }
 
-    for (file, line_analysis) in analysis.iter() {
-        if config.exclude_path(file) {
+    add_line_analysis(analysis, config, &mut result);
+    Ok(result)
+}
+
+fn add_line_analysis(
+    in_analysis: &HashMap<PathBuf, LineAnalysis>,
+    in_config: &Config,
+    in_out_trace: &mut TraceMap,
+) {
+    for (file, line_analysis) in in_analysis.iter() {
+        if in_config.exclude_path(file) || !in_config.include_path(file) {
             continue;
         }
         for line in &line_analysis.cover {
             let line = *line as u64;
-            if !result.contains_location(file, line) && !line_analysis.should_ignore(line as usize)
+            if !in_out_trace.contains_location(file, line)
+                && !line_analysis.should_ignore(line as usize)
             {
-                let rpath = config.strip_base_dir(file);
+                let rpath = in_config.strip_base_dir(file);
                 trace!(
                     "Adding trace for potentially uncoverable line in {}:{}",
                     rpath.display(),
                     line
                 );
-                result.add_trace(file, Trace::new_stub(line));
+                in_out_trace.add_trace(file, Trace::new_stub(line));
             }
         }
     }
-    Ok(result)
 }
 
 #[cfg(ptrace_supported)]