Use rstest

This commit is contained in:
Ajeet D'Souza 2021-05-17 21:31:50 +05:30
parent a4fcb39c8b
commit 47f0570f74
16 changed files with 480 additions and 511 deletions

64
Cargo.lock generated
View File

@ -335,12 +335,6 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "once_cell"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]] [[package]]
name = "ordered-float" name = "ordered-float"
version = "2.2.0" version = "2.2.0"
@ -356,6 +350,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.10" version = "0.2.10"
@ -554,6 +557,28 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "rstest"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "041bb0202c14f6a158bbbf086afb03d0c6e975c2dec7d4912f8061ed44f290af"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"rustc_version",
"syn",
]
[[package]]
name = "rustc_version"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
dependencies = [
"semver",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.5"
@ -561,10 +586,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]] [[package]]
name = "seq-macro" name = "semver"
version = "0.2.2" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]] [[package]]
name = "serde" name = "serde"
@ -653,6 +690,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.7.1" version = "1.7.1"
@ -754,10 +797,9 @@ dependencies = [
"dirs-next", "dirs-next",
"dunce", "dunce",
"glob", "glob",
"once_cell",
"ordered-float", "ordered-float",
"rand 0.7.3", "rand 0.7.3",
"seq-macro", "rstest",
"serde", "serde",
"tempfile", "tempfile",
] ]

View File

@ -26,8 +26,7 @@ rand = "0.7.3"
[dev-dependencies] [dev-dependencies]
assert_cmd = "1.0.1" assert_cmd = "1.0.1"
once_cell = "1.4.1" rstest = "0.10.0"
seq-macro = "0.2.1"
[build-dependencies] [build-dependencies]
clap = "3.0.0-beta.2" clap = "3.0.0-beta.2"

7
rustfmt.toml Normal file
View File

@ -0,0 +1,7 @@
# group_imports = "StdExternalCrate"
# imports_granularity = "Module"
newline_style = "Native"
use_field_init_shorthand = true
use_small_heuristics = "Max"
use_try_shorthand = true
# wrap_comments = true

View File

@ -126,9 +126,6 @@ pub struct Remove {
// Use interactive selection // Use interactive selection
#[clap(conflicts_with = "path", long, short, value_name = "keywords")] #[clap(conflicts_with = "path", long, short, value_name = "keywords")]
pub interactive: Option<Vec<String>>, pub interactive: Option<Vec<String>>,
#[clap( #[clap(conflicts_with = "interactive", required_unless_present = "interactive")]
conflicts_with = "interactive",
required_unless_present = "interactive"
)]
pub path: Option<String>, pub path: Option<String>,
} }

View File

@ -13,10 +13,7 @@ impl Run for Add {
util::resolve_path(&self.path) util::resolve_path(&self.path)
}?; }?;
if config::zo_exclude_dirs()? if config::zo_exclude_dirs()?.iter().any(|pattern| pattern.matches_path(&path)) {
.iter()
.any(|pattern| pattern.matches_path(&path))
{
return Ok(()); return Ok(());
} }

View File

@ -31,11 +31,8 @@ fn from_autojump<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
let buffer = fs::read_to_string(path) let buffer = fs::read_to_string(path)
.with_context(|| format!("could not open autojump database: {}", path.display()))?; .with_context(|| format!("could not open autojump database: {}", path.display()))?;
let mut dirs = db let mut dirs =
.dirs db.dirs.iter().map(|dir| (dir.path.as_ref(), dir.clone())).collect::<HashMap<_, _>>();
.iter()
.map(|dir| (dir.path.as_ref(), dir.clone()))
.collect::<HashMap<_, _>>();
for line in buffer.lines() { for line in buffer.lines() {
if line.is_empty() { if line.is_empty() {
@ -43,24 +40,16 @@ fn from_autojump<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
} }
let mut split = line.splitn(2, '\t'); let mut split = line.splitn(2, '\t');
let rank = split let rank = split.next().with_context(|| format!("invalid entry: {}", line))?;
.next() let mut rank = rank.parse::<f64>().with_context(|| format!("invalid rank: {}", rank))?;
.with_context(|| format!("invalid entry: {}", line))?;
let mut rank = rank
.parse::<f64>()
.with_context(|| format!("invalid rank: {}", rank))?;
// Normalize the rank using a sigmoid function. Don't import actual // Normalize the rank using a sigmoid function. Don't import actual
// ranks from autojump, since its scoring algorithm is very different, // ranks from autojump, since its scoring algorithm is very different,
// and might take a while to get normalized. // and might take a while to get normalized.
rank = 1.0 / (1.0 + (-rank).exp()); rank = 1.0 / (1.0 + (-rank).exp());
let path = split let path = split.next().with_context(|| format!("invalid entry: {}", line))?;
.next()
.with_context(|| format!("invalid entry: {}", line))?;
dirs.entry(path) dirs.entry(path).and_modify(|dir| dir.rank += rank).or_insert_with(|| Dir {
.and_modify(|dir| dir.rank += rank)
.or_insert_with(|| Dir {
path: path.to_string().into(), path: path.to_string().into(),
rank, rank,
last_accessed: 0, last_accessed: 0,
@ -78,11 +67,8 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
let buffer = fs::read_to_string(path) let buffer = fs::read_to_string(path)
.with_context(|| format!("could not open z database: {}", path.display()))?; .with_context(|| format!("could not open z database: {}", path.display()))?;
let mut dirs = db let mut dirs =
.dirs db.dirs.iter().map(|dir| (dir.path.as_ref(), dir.clone())).collect::<HashMap<_, _>>();
.iter()
.map(|dir| (dir.path.as_ref(), dir.clone()))
.collect::<HashMap<_, _>>();
for line in buffer.lines() { for line in buffer.lines() {
if line.is_empty() { if line.is_empty() {
@ -90,23 +76,14 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
} }
let mut split = line.rsplitn(3, '|'); let mut split = line.rsplitn(3, '|');
let last_accessed = split let last_accessed = split.next().with_context(|| format!("invalid entry: {}", line))?;
.next() let last_accessed =
.with_context(|| format!("invalid entry: {}", line))?; last_accessed.parse().with_context(|| format!("invalid epoch: {}", last_accessed))?;
let last_accessed = last_accessed
.parse()
.with_context(|| format!("invalid epoch: {}", last_accessed))?;
let rank = split let rank = split.next().with_context(|| format!("invalid entry: {}", line))?;
.next() let rank = rank.parse().with_context(|| format!("invalid rank: {}", rank))?;
.with_context(|| format!("invalid entry: {}", line))?;
let rank = rank
.parse()
.with_context(|| format!("invalid rank: {}", rank))?;
let path = split let path = split.next().with_context(|| format!("invalid entry: {}", line))?;
.next()
.with_context(|| format!("invalid entry: {}", line))?;
dirs.entry(path) dirs.entry(path)
.and_modify(|dir| { .and_modify(|dir| {
@ -115,11 +92,7 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
dir.last_accessed = last_accessed; dir.last_accessed = last_accessed;
} }
}) })
.or_insert(Dir { .or_insert(Dir { path: path.to_string().into(), rank, last_accessed });
path: path.to_string().into(),
rank,
last_accessed,
});
} }
db.dirs = DirList(dirs.into_iter().map(|(_, dir)| dir).collect()); db.dirs = DirList(dirs.into_iter().map(|(_, dir)| dir).collect());

View File

@ -10,21 +10,12 @@ use std::io::{self, Write};
impl Run for Init { impl Run for Init {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let cmd = if self.no_aliases { let cmd = if self.no_aliases { None } else { Some(self.cmd.as_str()) };
None
} else {
Some(self.cmd.as_str())
};
let echo = config::zo_echo(); let echo = config::zo_echo();
let resolve_symlinks = config::zo_resolve_symlinks(); let resolve_symlinks = config::zo_resolve_symlinks();
let opts = &Opts { let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks };
cmd,
hook: self.hook,
echo,
resolve_symlinks,
};
let source = match self.shell { let source = match self.shell {
InitShell::Bash => shell::Bash(opts).render(), InitShell::Bash => shell::Bash(opts).render(),

View File

@ -35,9 +35,7 @@ impl Run for Query {
if self.score { if self.score {
print!("{}", selection); print!("{}", selection);
} else { } else {
let path = selection let path = selection.get(5..).context("could not read selection from fzf")?;
.get(5..)
.context("could not read selection from fzf")?;
print!("{}", path) print!("{}", path)
} }
} else if self.list { } else if self.list {

View File

@ -34,9 +34,7 @@ pub fn zo_exclude_dirs() -> Result<Vec<Pattern>> {
match env::var_os("_ZO_EXCLUDE_DIRS") { match env::var_os("_ZO_EXCLUDE_DIRS") {
Some(dirs_osstr) => env::split_paths(&dirs_osstr) Some(dirs_osstr) => env::split_paths(&dirs_osstr)
.map(|path| { .map(|path| {
let pattern = path let pattern = path.to_str().context("invalid unicode in _ZO_EXCLUDE_DIRS")?;
.to_str()
.context("invalid unicode in _ZO_EXCLUDE_DIRS")?;
Pattern::new(&pattern) Pattern::new(&pattern)
.with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {}", pattern)) .with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {}", pattern))
}) })
@ -61,9 +59,7 @@ pub fn zo_fzf_opts() -> Option<OsString> {
pub fn zo_maxage() -> Result<Rank> { pub fn zo_maxage() -> Result<Rank> {
match env::var_os("_ZO_MAXAGE") { match env::var_os("_ZO_MAXAGE") {
Some(maxage_osstr) => { Some(maxage_osstr) => {
let maxage_str = maxage_osstr let maxage_str = maxage_osstr.to_str().context("invalid unicode in _ZO_MAXAGE")?;
.to_str()
.context("invalid unicode in _ZO_MAXAGE")?;
let maxage = maxage_str.parse::<u64>().with_context(|| { let maxage = maxage_str.parse::<u64>().with_context(|| {
format!("unable to parse _ZO_MAXAGE as integer: {}", maxage_str) format!("unable to parse _ZO_MAXAGE as integer: {}", maxage_str)
})?; })?;

View File

@ -20,9 +20,7 @@ impl DirList<'_> {
// Assume a maximum size for the database. This prevents bincode from // Assume a maximum size for the database. This prevents bincode from
// throwing strange errors when it encounters invalid data. // throwing strange errors when it encounters invalid data.
const MAX_SIZE: u64 = 32 << 20; // 32 MiB const MAX_SIZE: u64 = 32 << 20; // 32 MiB
let deserializer = &mut bincode::options() let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
.with_fixint_encoding()
.with_limit(MAX_SIZE);
// Split bytes into sections. // Split bytes into sections.
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _; let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
@ -36,11 +34,9 @@ impl DirList<'_> {
let version = deserializer.deserialize(bytes_version)?; let version = deserializer.deserialize(bytes_version)?;
match version { match version {
Self::VERSION => Ok(deserializer.deserialize(bytes_dirs)?), Self::VERSION => Ok(deserializer.deserialize(bytes_dirs)?),
version => bail!( version => {
"unsupported version (got {}, supports {})", bail!("unsupported version (got {}, supports {})", version, Self::VERSION,)
version, }
Self::VERSION,
),
} }
})() })()
.context("could not deserialize database") .context("could not deserialize database")
@ -159,11 +155,7 @@ mod tests {
#[test] #[test]
fn zero_copy() { fn zero_copy() {
let dirs = DirList(vec![Dir { let dirs = DirList(vec![Dir { path: "/".into(), rank: 0.0, last_accessed: 0 }]);
path: "/".into(),
rank: 0.0,
last_accessed: 0,
}]);
let bytes = dirs.to_bytes().unwrap(); let bytes = dirs.to_bytes().unwrap();
let dirs = DirList::from_bytes(&bytes).unwrap(); let dirs = DirList::from_bytes(&bytes).unwrap();

View File

@ -26,10 +26,7 @@ impl<'file> Database<'file> {
let buffer = self.dirs.to_bytes()?; let buffer = self.dirs.to_bytes()?;
let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| { let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| {
format!( format!("could not create temporary database in: {}", self.data_dir.display())
"could not create temporary database in: {}",
self.data_dir.display()
)
})?; })?;
// Preallocate enough space on the file, preventing copying later on. // Preallocate enough space on the file, preventing copying later on.
@ -37,10 +34,7 @@ impl<'file> Database<'file> {
// ignore it and proceed. // ignore it and proceed.
let _ = file.as_file().set_len(buffer.len() as _); let _ = file.as_file().set_len(buffer.len() as _);
file.write_all(&buffer).with_context(|| { file.write_all(&buffer).with_context(|| {
format!( format!("could not write to temporary database: {}", file.path().display())
"could not write to temporary database: {}",
file.path().display()
)
})?; })?;
let path = db_path(&self.data_dir); let path = db_path(&self.data_dir);
@ -56,11 +50,9 @@ impl<'file> Database<'file> {
let path = path.as_ref(); let path = path.as_ref();
match self.dirs.iter_mut().find(|dir| dir.path == path) { match self.dirs.iter_mut().find(|dir| dir.path == path) {
None => self.dirs.push(Dir { None => {
path: Cow::Owned(path.into()), self.dirs.push(Dir { path: Cow::Owned(path.into()), last_accessed: now, rank: 1.0 })
last_accessed: now, }
rank: 1.0,
}),
Some(dir) => { Some(dir) => {
dir.last_accessed = now; dir.last_accessed = now;
dir.rank += 1.0; dir.rank += 1.0;
@ -160,10 +152,7 @@ pub struct DatabaseFile {
impl DatabaseFile { impl DatabaseFile {
pub fn new<P: Into<PathBuf>>(data_dir: P) -> Self { pub fn new<P: Into<PathBuf>>(data_dir: P) -> Self {
DatabaseFile { DatabaseFile { buffer: Vec::new(), data_dir: data_dir.into() }
buffer: Vec::new(),
data_dir: data_dir.into(),
}
} }
pub fn open(&mut self) -> Result<Database> { pub fn open(&mut self) -> Result<Database> {
@ -177,27 +166,16 @@ impl DatabaseFile {
let dirs = DirList::from_bytes(&self.buffer).with_context(|| { let dirs = DirList::from_bytes(&self.buffer).with_context(|| {
format!("could not deserialize database: {}", path.display()) format!("could not deserialize database: {}", path.display())
})?; })?;
Ok(Database { Ok(Database { dirs, modified: false, data_dir: &self.data_dir })
dirs,
modified: false,
data_dir: &self.data_dir,
})
} }
Err(e) if e.kind() == io::ErrorKind::NotFound => { Err(e) if e.kind() == io::ErrorKind::NotFound => {
// Create data directory, but don't create any file yet. // Create data directory, but don't create any file yet.
// The file will be created later by [`Database::save`] // The file will be created later by [`Database::save`]
// if any data is modified. // if any data is modified.
fs::create_dir_all(&self.data_dir).with_context(|| { fs::create_dir_all(&self.data_dir).with_context(|| {
format!( format!("unable to create data directory: {}", self.data_dir.display())
"unable to create data directory: {}",
self.data_dir.display()
)
})?; })?;
Ok(Database { Ok(Database { dirs: DirList::new(), modified: false, data_dir: &self.data_dir })
dirs: DirList::new(),
modified: false,
data_dir: &self.data_dir,
})
} }
Err(e) => { Err(e) => {
Err(e).with_context(|| format!("could not read from database: {}", path.display())) Err(e).with_context(|| format!("could not read from database: {}", path.display()))
@ -217,11 +195,7 @@ mod tests {
#[test] #[test]
fn add() { fn add() {
let path = if cfg!(windows) { let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
r"C:\foo\bar"
} else {
"/foo/bar"
};
let now = 946684800; let now = 946684800;
let data_dir = tempfile::tempdir().unwrap(); let data_dir = tempfile::tempdir().unwrap();
@ -244,11 +218,7 @@ mod tests {
#[test] #[test]
fn remove() { fn remove() {
let path = if cfg!(windows) { let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
r"C:\foo\bar"
} else {
"/foo/bar"
};
let now = 946684800; let now = 946684800;
let data_dir = tempfile::tempdir().unwrap(); let data_dir = tempfile::tempdir().unwrap();

View File

@ -24,8 +24,7 @@ pub struct Stream<'db, 'file> {
impl<'db, 'file> Stream<'db, 'file> { impl<'db, 'file> Stream<'db, 'file> {
pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self { pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self {
// Iterate in descending order of score. // Iterate in descending order of score.
db.dirs db.dirs.sort_unstable_by_key(|dir| OrderedFloat(dir.score(now)));
.sort_unstable_by_key(|dir| OrderedFloat(dir.score(now)));
let idxs = (0..db.dirs.len()).rev(); let idxs = (0..db.dirs.len()).rev();
// If a directory is deleted and hasn't been used for 90 days, delete // If a directory is deleted and hasn't been used for 90 days, delete
@ -91,15 +90,9 @@ impl<'db, 'file> Stream<'db, 'file> {
return true; return true;
} }
let resolver = if self.resolve_symlinks { let resolver = if self.resolve_symlinks { fs::symlink_metadata } else { fs::metadata };
fs::symlink_metadata
} else {
fs::metadata
};
resolver(path.as_ref()) resolver(path.as_ref()).map(|m| m.is_dir()).unwrap_or_default()
.map(|m| m.is_dir())
.unwrap_or_default()
} }
fn matches_keywords<S: AsRef<str>>(&self, path: S) -> bool { fn matches_keywords<S: AsRef<str>>(&self, path: S) -> bool {
@ -135,42 +128,34 @@ impl<'db, 'file> Stream<'db, 'file> {
mod tests { mod tests {
use super::Database; use super::Database;
use rstest::rstest;
use std::path::PathBuf; use std::path::PathBuf;
#[test] #[rstest]
fn query() {
const CASES: &[(&[&str], &str, bool)] = &[
// Case normalization // Case normalization
(&["fOo", "bAr"], "/foo/bar", true), #[case(&["fOo", "bAr"], "/foo/bar", true)]
// Last component // Last component
(&["ba"], "/foo/bar", true), #[case(&["ba"], "/foo/bar", true)]
(&["fo"], "/foo/bar", false), #[case(&["fo"], "/foo/bar", false)]
// Slash as suffix // Slash as suffix
(&["foo/"], "/foo", false), #[case(&["foo/"], "/foo", false)]
(&["foo/"], "/foo/bar", true), #[case(&["foo/"], "/foo/bar", true)]
(&["foo/"], "/foo/bar/baz", false), #[case(&["foo/"], "/foo/bar/baz", false)]
(&["foo", "/"], "/foo", false), #[case(&["foo", "/"], "/foo", false)]
(&["foo", "/"], "/foo/bar", true), #[case(&["foo", "/"], "/foo/bar", true)]
(&["foo", "/"], "/foo/bar/baz", true), #[case(&["foo", "/"], "/foo/bar/baz", true)]
// Split components // Split components
(&["/", "fo", "/", "ar"], "/foo/bar", true), #[case(&["/", "fo", "/", "ar"], "/foo/bar", true)]
(&["oo/ba"], "/foo/bar", true), #[case(&["oo/ba"], "/foo/bar", true)]
// Overlap // Overlap
(&["foo", "o", "bar"], "/foo/bar", false), #[case(&["foo", "o", "bar"], "/foo/bar", false)]
(&["/foo/", "/bar"], "/foo/bar", false), #[case(&["/foo/", "/bar"], "/foo/bar", false)]
(&["/foo/", "/bar"], "/foo/baz/bar", true), #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)]
]; fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
let mut db =
let mut db = Database { Database { dirs: Vec::new().into(), modified: false, data_dir: &PathBuf::new() };
dirs: Vec::new().into(), let stream = db.stream(0).with_keywords(keywords);
modified: false,
data_dir: &PathBuf::new(),
};
let now = 0;
for &(keywords, path, is_match) in CASES {
let stream = db.stream(now).with_keywords(keywords);
assert_eq!(is_match, stream.matches_keywords(path)); assert_eq!(is_match, stream.matches_keywords(path));
} }
} }
}

View File

@ -16,10 +16,7 @@ impl Fzf {
if multiple { if multiple {
command.arg("-m"); command.arg("-m");
} }
command command.arg("-n2..").stdin(Stdio::piped()).stdout(Stdio::piped());
.arg("-n2..")
.stdin(Stdio::piped())
.stdout(Stdio::piped());
if let Some(fzf_opts) = config::zo_fzf_opts() { if let Some(fzf_opts) = config::zo_fzf_opts() {
command.env("FZF_DEFAULT_OPTS", fzf_opts); command.env("FZF_DEFAULT_OPTS", fzf_opts);
} }
@ -41,10 +38,7 @@ impl Fzf {
} }
pub fn wait_select(self) -> Result<String> { pub fn wait_select(self) -> Result<String> {
let output = self let output = self.child.wait_with_output().context("wait failed on fzf")?;
.child
.wait_with_output()
.context("wait failed on fzf")?;
match output.status.code() { match output.status.code() {
// normal exit // normal exit

View File

@ -39,57 +39,18 @@ mod tests {
use askama::Template; use askama::Template;
use assert_cmd::Command; use assert_cmd::Command;
use once_cell::sync::OnceCell; use rstest::rstest;
use seq_macro::seq;
macro_rules! with_opts_size { #[rstest]
($macro:ident) => { fn bash_bash(
$macro!(24); #[values(None, Some("z"))] cmd: Option<&str>,
}; #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
} #[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Bash(&opts).render().unwrap();
fn opts() -> &'static [Opts<'static>] {
static OPTS: OnceCell<Vec<Opts>> = OnceCell::new();
const BOOLS: &[bool] = &[false, true];
const HOOKS: &[InitHook] = &[InitHook::None, InitHook::Prompt, InitHook::Pwd];
const CMDS: &[Option<&str>] = &[None, Some("z")];
OPTS.get_or_init(|| {
let mut opts = Vec::new();
for &echo in BOOLS {
for &resolve_symlinks in BOOLS {
for &hook in HOOKS {
for &cmd in CMDS {
opts.push(Opts {
cmd,
hook,
echo,
resolve_symlinks,
});
}
}
}
}
// Verify that the value hardcoded into `with_opts_size` is correct.
macro_rules! id {
($x:literal) => {
$x
};
}
assert_eq!(opts.len(), with_opts_size!(id));
opts
})
}
macro_rules! make_tests {
($N:literal) => {
seq!(i in 0..$N {
#[test]
fn bash_bash_#i() {
let opts = dbg!(&opts()[i]);
let source = Bash(opts).render().unwrap();
Command::new("bash") Command::new("bash")
.args(&["--noprofile", "--norc", "-c", &source]) .args(&["--noprofile", "--norc", "-c", &source])
.assert() .assert()
@ -98,10 +59,16 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn bash_shellcheck_#i() { fn bash_shellcheck(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Bash(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Bash(&opts).render().unwrap();
Command::new("shellcheck") Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "bash", "-"]) .args(&["--enable", "all", "--shell", "bash", "-"])
.write_stdin(source) .write_stdin(source)
@ -111,10 +78,15 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn bash_shfmt_#i() { fn bash_shfmt(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let mut source = Bash(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Bash(&opts).render().unwrap();
source.push('\n'); source.push('\n');
Command::new("shfmt") Command::new("shfmt")
@ -126,18 +98,20 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn elvish_elvish_#i() { fn elvish_elvish(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = String::new(); let mut source = String::new();
// Filter out lines using edit:*, since those functions // Filter out lines using edit:*, since those functions
// are only available in the interactive editor. // are only available in the interactive editor.
for line in Elvish(opts) for line in
.render() Elvish(&opts).render().unwrap().split('\n').filter(|line| !line.contains("edit:"))
.unwrap()
.split('\n')
.filter(|line| !line.contains("edit:"))
{ {
source.push_str(line); source.push_str(line);
source.push('\n'); source.push('\n');
@ -151,10 +125,15 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn fish_fish_#i() { fn fish_fish(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Fish(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Fish(&opts).render().unwrap();
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap(); let tempdir = tempdir.path().to_str().unwrap();
@ -168,10 +147,15 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn fish_fishindent_#i() { fn fish_fishindent(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let mut source = Fish(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Fish(&opts).render().unwrap();
source.push('\n'); source.push('\n');
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
@ -187,10 +171,15 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn nushell_nushell_#i() { fn nushell_nushell(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Nushell(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Nushell(&opts).render().unwrap();
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap(); let tempdir = tempdir.path().to_str().unwrap();
@ -207,10 +196,16 @@ mod tests {
} }
} }
#[test] #[rstest]
fn posix_bashposix_#i() { fn posix_bashposix(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Posix(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap();
let assert = Command::new("bash") let assert = Command::new("bash")
.args(&["--posix", "--noprofile", "--norc", "-c", &source]) .args(&["--posix", "--noprofile", "--norc", "-c", &source])
.assert() .assert()
@ -222,25 +217,32 @@ mod tests {
} }
} }
#[test] #[rstest]
fn posix_dash_#i() { fn posix_dash(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Posix(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
let assert = Command::new("dash") #[values(false, true)] echo: bool,
.args(&["-c", &source]) #[values(false, true)] resolve_symlinks: bool,
.assert() ) {
.success() let opts = Opts { cmd, hook, echo, resolve_symlinks };
.stderr(""); let source = Posix(&opts).render().unwrap();
let assert = Command::new("dash").args(&["-c", &source]).assert().success().stderr("");
if opts.hook != InitHook::Pwd { if opts.hook != InitHook::Pwd {
assert.stdout(""); assert.stdout("");
} }
} }
#[test] #[rstest]
fn posix_shellcheck_#i() { fn posix_shellcheck_(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Posix(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap();
Command::new("shellcheck") Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "sh", "-"]) .args(&["--enable", "all", "--shell", "sh", "-"])
.write_stdin(source) .write_stdin(source)
@ -250,11 +252,17 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn posix_shfmt_#i() { fn posix_shfmt(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let mut source = Posix(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Posix(&opts).render().unwrap();
source.push('\n'); source.push('\n');
Command::new("shfmt") Command::new("shfmt")
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"]) .args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"])
.write_stdin(source) .write_stdin(source)
@ -264,10 +272,16 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn powershell_pwsh_#i() { fn powershell_pwsh(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Powershell(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Powershell(&opts).render().unwrap();
Command::new("pwsh") Command::new("pwsh")
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source]) .args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source])
.assert() .assert()
@ -276,11 +290,17 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn xonsh_black_#i() { fn xonsh_black(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let mut source = Xonsh(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Xonsh(&opts).render().unwrap();
source.push('\n'); source.push('\n');
Command::new("black") Command::new("black")
.args(&["--check", "--diff", "-"]) .args(&["--check", "--diff", "-"])
.write_stdin(source) .write_stdin(source)
@ -289,22 +309,30 @@ mod tests {
.stdout(""); .stdout("");
} }
#[test] #[rstest]
fn xonsh_mypy_#i() { fn xonsh_mypy(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Xonsh(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
Command::new("mypy") #[values(false, true)] echo: bool,
.args(&["--command", &source]) #[values(false, true)] resolve_symlinks: bool,
.assert() ) {
.success() let opts = Opts { cmd, hook, echo, resolve_symlinks };
.stderr(""); let source = Xonsh(&opts).render().unwrap();
Command::new("mypy").args(&["--command", &source]).assert().success().stderr("");
} }
#[test] #[rstest]
fn xonsh_pylint_#i() { fn xonsh_pylint(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let mut source = Xonsh(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Xonsh(&opts).render().unwrap();
source.push('\n'); source.push('\n');
Command::new("pylint") Command::new("pylint")
.args(&["--from-stdin", "zoxide"]) .args(&["--from-stdin", "zoxide"])
.write_stdin(source) .write_stdin(source)
@ -313,10 +341,15 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn xonsh_xonsh_#i() { fn xonsh_xonsh(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Xonsh(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Xonsh(&opts).render().unwrap();
// We can't pass the source directly to `xonsh -c` due to // We can't pass the source directly to `xonsh -c` due to
// a bug: <https://github.com/xonsh/xonsh/issues/3959> // a bug: <https://github.com/xonsh/xonsh/issues/3959>
@ -324,7 +357,7 @@ mod tests {
.args(&[ .args(&[
"-c", "-c",
"import sys; execx(sys.stdin.read(), 'exec', __xonsh__.ctx, filename='zoxide')", "import sys; execx(sys.stdin.read(), 'exec', __xonsh__.ctx, filename='zoxide')",
"--no-rc" "--no-rc",
]) ])
.write_stdin(source.as_bytes()) .write_stdin(source.as_bytes())
.assert() .assert()
@ -333,10 +366,16 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn zsh_shellcheck_#i() { fn zsh_shellcheck(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Zsh(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Zsh(&opts).render().unwrap();
// ShellCheck doesn't support zsh yet. // ShellCheck doesn't support zsh yet.
// https://github.com/koalaman/shellcheck/issues/809 // https://github.com/koalaman/shellcheck/issues/809
Command::new("shellcheck") Command::new("shellcheck")
@ -348,10 +387,16 @@ mod tests {
.stderr(""); .stderr("");
} }
#[test] #[rstest]
fn zsh_zsh_#i() { fn zsh_zsh(
let opts = dbg!(&opts()[i]); #[values(None, Some("z"))] cmd: Option<&str>,
let source = Zsh(opts).render().unwrap(); #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Zsh(&opts).render().unwrap();
Command::new("zsh") Command::new("zsh")
.args(&["-c", &source, "--no-rcs"]) .args(&["-c", &source, "--no-rcs"])
.assert() .assert()
@ -359,9 +404,4 @@ mod tests {
.stdout("") .stdout("")
.stderr(""); .stderr("");
} }
});
}
}
with_opts_size!(make_tests);
} }

View File

@ -26,8 +26,7 @@ pub fn current_time() -> Result<Epoch> {
pub fn path_to_str<P: AsRef<Path>>(path: &P) -> Result<&str> { pub fn path_to_str<P: AsRef<Path>>(path: &P) -> Result<&str> {
let path = path.as_ref(); let path = path.as_ref();
path.to_str() path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display()))
.with_context(|| format!("invalid unicode in path: {}", path.display()))
} }
/// Resolves the absolute version of a path. /// Resolves the absolute version of a path.

View File

@ -38,13 +38,7 @@ fn completions_fish() {
fn completions_powershell() { fn completions_powershell() {
let source = include_str!("../contrib/completions/_zoxide.ps1"); let source = include_str!("../contrib/completions/_zoxide.ps1");
Command::new("pwsh") Command::new("pwsh")
.args(&[ .args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", source])
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-Command",
source,
])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")
@ -62,10 +56,5 @@ fn completions_zsh() {
compinit -u compinit -u
"#; "#;
Command::new("zsh") Command::new("zsh").args(&["-c", source, "--no-rcs"]).assert().success().stdout("").stderr("");
.args(&["-c", source, "--no-rcs"])
.assert()
.success()
.stdout("")
.stderr("");
} }