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",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"fastrand 2.0.1",
|
||||
"fork",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
|
|
@ -1104,6 +1105,8 @@ dependencies = [
|
|||
"rust-embed",
|
||||
"serde",
|
||||
"systemicons",
|
||||
"tempfile",
|
||||
"test-log",
|
||||
"tokio",
|
||||
"trash",
|
||||
]
|
||||
|
|
@ -5014,6 +5017,27 @@ dependencies = [
|
|||
"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]]
|
||||
name = "thiserror"
|
||||
version = "1.0.56"
|
||||
|
|
|
|||
|
|
@ -47,3 +47,10 @@ debug = true
|
|||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
#[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