Test helpers and unit tests (#26)
* Implement a few utility functions for tests Most tests would require a test file hierarchy instead of operating on a live system. * Add unit tests for `tab::scan_path` Tests: * Works on a valid path * Returns an empty Vec on an invalid directory * Returns an empty Vec for an empty directory I also implemented a few test helpers that may be useful for other unit tests. * Less spammy test logs and placate Clippy.
This commit is contained in:
parent
60eeb724d1
commit
d6c58991c0
4 changed files with 228 additions and 0 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -1092,6 +1092,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"fastrand 2.0.1",
|
||||||
"fork",
|
"fork",
|
||||||
"i18n-embed",
|
"i18n-embed",
|
||||||
"i18n-embed-fl",
|
"i18n-embed-fl",
|
||||||
|
|
@ -1104,6 +1105,8 @@ dependencies = [
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"systemicons",
|
"systemicons",
|
||||||
|
"tempfile",
|
||||||
|
"test-log",
|
||||||
"tokio",
|
"tokio",
|
||||||
"trash",
|
"trash",
|
||||||
]
|
]
|
||||||
|
|
@ -5014,6 +5017,27 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-log"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6159ab4116165c99fc88cce31f99fa2c9dbe08d3691cb38da02fc3b45f357d2b"
|
||||||
|
dependencies = [
|
||||||
|
"env_logger",
|
||||||
|
"test-log-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-log-macros"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ba277e77219e9eea169e8508942db1bf5d8a41ff2db9b20aab5a5aadc9fa25d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.56"
|
version = "1.0.56"
|
||||||
|
|
|
||||||
|
|
@ -47,3 +47,10 @@ debug = true
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
fork = "0.1"
|
fork = "0.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
# cap-std = "3"
|
||||||
|
# cap-tempfile = "3"
|
||||||
|
fastrand = "2"
|
||||||
|
tempfile = "3"
|
||||||
|
test-log = "0.2"
|
||||||
|
|
|
||||||
127
src/main.rs
127
src/main.rs
|
|
@ -1137,3 +1137,130 @@ impl Application for App {
|
||||||
Subscription::batch(subscriptions)
|
Subscription::batch(subscriptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utilities to build a temporary file hierarchy for tests.
|
||||||
|
//
|
||||||
|
// Ideally, tests would use the cap-std crate which limits path traversal.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_utils {
|
||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
fs::File,
|
||||||
|
io::{self, Write},
|
||||||
|
iter,
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::{debug, trace};
|
||||||
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
|
use crate::tab::Item;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Default number of files, directories, and nested directories for test file system
|
||||||
|
pub const NUM_FILES: usize = 2;
|
||||||
|
pub const NUM_DIRS: usize = 2;
|
||||||
|
pub const NUM_NESTED: usize = 1;
|
||||||
|
pub const NAME_LEN: usize = 5;
|
||||||
|
|
||||||
|
/// Add `n` temporary files in `dir`
|
||||||
|
///
|
||||||
|
/// Each file is assigned a numeric name from [0, n).
|
||||||
|
pub fn file_flat_hier<D: AsRef<Path>>(dir: D, n: usize) -> io::Result<Vec<File>> {
|
||||||
|
let dir = dir.as_ref();
|
||||||
|
(0..n)
|
||||||
|
.map(|i| -> io::Result<File> {
|
||||||
|
let name = i.to_string();
|
||||||
|
let path = dir.join(&name);
|
||||||
|
|
||||||
|
let mut file = File::create(path)?;
|
||||||
|
file.write_all(name.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random alphanumeric String of length `len`
|
||||||
|
fn rand_string(len: usize) -> String {
|
||||||
|
(0..len).map(|_| fastrand::alphanumeric()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a small, temporary file hierarchy.
|
||||||
|
pub fn simple_fs(
|
||||||
|
files: usize,
|
||||||
|
dirs: usize,
|
||||||
|
nested: usize,
|
||||||
|
name_len: usize,
|
||||||
|
) -> io::Result<TempDir> {
|
||||||
|
// Files created inside of a TempDir are deleted with the directory
|
||||||
|
// TempDir won't leak resources as long as the destructor runs
|
||||||
|
let root = tempdir()?;
|
||||||
|
debug!("Root temp directory: {}", root.as_ref().display());
|
||||||
|
|
||||||
|
// All paths for directories and nested directories
|
||||||
|
let paths = (0..dirs).flat_map(|_| {
|
||||||
|
let root = root.as_ref();
|
||||||
|
let current = rand_string(name_len);
|
||||||
|
|
||||||
|
iter::once(root.join(¤t)).chain(
|
||||||
|
(0..nested).map(move |_| root.join(format!("{current}/{}", rand_string(name_len)))),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create directories from `paths` and add a few files
|
||||||
|
for path in paths {
|
||||||
|
fs::create_dir_all(&path)?;
|
||||||
|
file_flat_hier(&path, files)?;
|
||||||
|
|
||||||
|
for entry in path.read_dir()? {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type()?.is_file() {
|
||||||
|
trace!("Created file: {}", entry.path().display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Empty file hierarchy
|
||||||
|
pub fn empty_fs() -> io::Result<TempDir> {
|
||||||
|
tempdir()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort files.
|
||||||
|
///
|
||||||
|
/// Directories are placed before files.
|
||||||
|
/// Files are lexically sorted.
|
||||||
|
/// This is more or less copied right from the [Tab] code
|
||||||
|
pub fn sort_files(a: &Path, b: &Path) -> Ordering {
|
||||||
|
match (a.is_dir(), b.is_dir()) {
|
||||||
|
(true, false) => Ordering::Less,
|
||||||
|
(false, true) => Ordering::Greater,
|
||||||
|
_ => lexical_sort::natural_lexical_cmp(
|
||||||
|
a.file_name()
|
||||||
|
.expect("temp entries should have names")
|
||||||
|
.to_str()
|
||||||
|
.expect("temp entries should be valid UTF-8"),
|
||||||
|
b.file_name()
|
||||||
|
.expect("temp entries should have names")
|
||||||
|
.to_str()
|
||||||
|
.expect("temp entries should be valid UTF-8"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equality for [Path] and [Item].
|
||||||
|
pub fn eq_path_item(path: &Path, item: &Item) -> bool {
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.expect("temp entries should have names")
|
||||||
|
.to_str()
|
||||||
|
.expect("temp entries should be valid UTF-8");
|
||||||
|
let metadata = path.is_dir();
|
||||||
|
|
||||||
|
name == item.name && metadata == item.metadata.is_dir() && path == item.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
70
src/tab.rs
70
src/tab.rs
|
|
@ -1013,3 +1013,73 @@ impl Tab {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
use super::scan_path;
|
||||||
|
use crate::test_utils::{
|
||||||
|
empty_fs, eq_path_item, simple_fs, sort_files, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_NESTED,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_path_succeeds_on_valid_path() -> io::Result<()> {
|
||||||
|
let fs = simple_fs(NUM_FILES, NUM_DIRS, NUM_NESTED, NAME_LEN)?;
|
||||||
|
let path = fs.path();
|
||||||
|
|
||||||
|
let mut entries: Vec<_> = path
|
||||||
|
.read_dir()?
|
||||||
|
.map(|maybe_entry| maybe_entry.map(|entry| entry.path()))
|
||||||
|
.collect::<io::Result<_>>()?;
|
||||||
|
entries.sort_by(|a, b| sort_files(a, b));
|
||||||
|
|
||||||
|
debug!("Calling scan_path(\"{}\")", path.display());
|
||||||
|
let actual = scan_path(&path.to_owned());
|
||||||
|
|
||||||
|
// scan_path shouldn't skip any entries
|
||||||
|
assert_eq!(entries.len(), actual.len());
|
||||||
|
|
||||||
|
// Correct files should be scanned
|
||||||
|
assert!(entries
|
||||||
|
.into_iter()
|
||||||
|
.zip(actual.into_iter())
|
||||||
|
.all(|(path, item)| eq_path_item(&path, &item)));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_path_returns_empty_vec_for_invalid_path() -> io::Result<()> {
|
||||||
|
let fs = simple_fs(NUM_FILES, NUM_DIRS, NUM_NESTED, NAME_LEN)?;
|
||||||
|
let path = fs.path();
|
||||||
|
|
||||||
|
// A nonexisting path within the temp dir
|
||||||
|
let invalid_path = path.join("ferris");
|
||||||
|
assert!(!invalid_path.exists());
|
||||||
|
|
||||||
|
debug!("Calling scan_path(\"{}\")", invalid_path.display());
|
||||||
|
let actual = scan_path(&invalid_path);
|
||||||
|
|
||||||
|
assert!(actual.is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_path_empty_dir_returns_empty_vec() -> io::Result<()> {
|
||||||
|
let fs = empty_fs()?;
|
||||||
|
let path = fs.path();
|
||||||
|
|
||||||
|
debug!("Calling scan_path(\"{}\")", path.display());
|
||||||
|
let actual = scan_path(&path.to_owned());
|
||||||
|
|
||||||
|
assert_eq!(0, path.read_dir()?.count());
|
||||||
|
assert_eq!(0, actual.len());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue