From a638532e08b896f18fd2e452a17b77556d22bb3f Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez Lopez Date: Wed, 15 Nov 2023 00:31:10 -0500 Subject: [PATCH] add tests, break stuff + new -y, --delete option to always delete --- Cargo.lock | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 ++ README.md | 8 ++-- src/lib.rs | 116 +++++++-------------------------------------- src/main.rs | 92 +++++++++++++++++++++++++++++++++++- tests/mod.rs | 40 ++++++++++++++++ 6 files changed, 283 insertions(+), 105 deletions(-) create mode 100644 tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 37d06f8..6a8846f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,11 +71,27 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "assert_cmd" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "atmpt" version = "0.1.0" dependencies = [ "anyhow", + "assert_cmd", "chrono", "clap", "directories-next", @@ -99,6 +115,17 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -186,6 +213,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "directories-next" version = "2.0.0" @@ -207,6 +240,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "getrandom" version = "0.2.11" @@ -247,6 +292,15 @@ dependencies = [ "cc", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "js-sys" version = "0.3.65" @@ -279,6 +333,12 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + [[package]] name = "num-traits" version = "0.2.17" @@ -294,6 +354,34 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "predicates" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" +dependencies = [ + "anstyle", + "difflib", + "itertools", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -332,6 +420,32 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strsim" version = "0.10.0" @@ -349,6 +463,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.50" @@ -381,6 +501,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 68f81b8..545c22f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,6 @@ anyhow = "1.0.75" directories-next = "2.0.0" clap = { version = "4.4.8", features = ["derive", "env"] } chrono = "0.4.31" + +[dev-dependencies] +assert_cmd = "2.0.12" diff --git a/README.md b/README.md index 1efd75b..403ca82 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ variable. This makes testing some quick lines of code easy without having to open a new replit or creating a new project with boilerplate yourself. -### Below is a showcase with the `$VISUAL` variable set to `nvim` for Neovim: +### Below is a showcase with the `$VISUAL` variable set to `nvim` for Neovim: (old) [![asciicast](https://asciinema.org/a/5soMz3UBzMbXO2Nb7LQELtcsT.svg)](https://asciinema.org/a/5soMz3UBzMbXO2Nb7LQELtcsT) @@ -44,8 +44,8 @@ Finally, you can create any templates you would like to use in atmpt's may copy them over: ```bash -mkdir -p $(atmpt --template-dir) -cp -r templates/* $(atmpt --template-dir) +mkdir -p $(atmpt --list-template-dir) +cp -r templates/* $(atmpt --list-template-dir) ``` ## Data Directory @@ -56,7 +56,7 @@ Atmpt offers an option to print it out on your system (you may have seen its output be used in the [installing] section): ```bash -atmpt --template-dir +atmpt --list-template-dir ``` _(The option could be shortened to `-d`)_ diff --git a/src/lib.rs b/src/lib.rs index 4371777..b85bc48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,118 +1,36 @@ -use std::{env, fs, io, path::Path, process::Command}; - -use anyhow::{bail, Ok}; -use chrono::Local; use clap::Parser; -use directories_next::ProjectDirs; -use templates::Templates; pub mod templates; +pub const EDITOR_KEY: &str = "VISUAL"; +pub const ALWAYS_DELETE_KEY: &str = "ATMPT_ALWAYS_DELETE"; +pub const TEMPLATE_DIR_KEY: &str = "ATMPT_DATA_DIR"; + #[derive(Debug, Parser)] #[command(author, version, about)] pub struct Atmpt { #[command(flatten)] - required: RequiredArgs, + pub required: RequiredArgs, + + #[arg(short, long, env = EDITOR_KEY, help = "Use given editor for this run")] + pub editor: Option, - #[arg(short, long, env = "VISUAL", help = "Use given editor for this run")] - editor: Option, + #[arg(short = 'y', long, env = ALWAYS_DELETE_KEY, help = "Autodelete project on exit")] + pub delete: bool, + + #[arg(long, hide = true, env = TEMPLATE_DIR_KEY)] // override template dir + pub template_dir: Option, } #[derive(Debug, Parser)] #[group(required = true)] pub struct RequiredArgs { #[arg(group = "main")] - template: Option, + pub template: Option, - #[arg( - group = "main", - short = 'd', - long = "template-dir", - help = "Output template directory" - )] - list_template_dir: bool, + #[arg(group = "main", short = 'd', long, help = "Output template directory")] + pub list_template_dir: bool, #[arg(group = "main", short, long, help = "List available templates")] - list_templates: bool, -} - -impl Atmpt { - pub fn parse_with(dirs: &ProjectDirs) -> anyhow::Result<()> { - let args = Self::parse(); - let req = args.required; - let data_dir = dirs.data_dir(); - - if let Some(template) = req.template { - let Some(editor) = args.editor else { - bail!("No editor to use!"); // really should not happen - }; - - try_template(&template, &editor, data_dir)?; - } else if req.list_template_dir { - println!("{}", data_dir.display()); - } else { - println!("{}", Templates::try_from(data_dir)?); - } - - Ok(()) - } -} - -fn try_template(template: &str, editor: &str, data_dir: &Path) -> anyhow::Result<()> { - let templates = Templates::try_from(data_dir)?; - let wanted_dir = templates.find(template)?; - - let time = Local::now().format("%Y_%m_%d-%H_%M_%S"); - let tmp_dir = env::temp_dir() - .join("atmpt") // store tmp projects in folder - .join(format!("{template}_{time}")); - copy_dir_recursively(wanted_dir, &tmp_dir)?; - - std::env::set_current_dir(&tmp_dir).expect("Could not change to temp directory!"); - Command::new(editor) - .arg(&tmp_dir) - .spawn()? - .wait() - .expect("Could not launch editor!"); - - if ask_y_n("Would you like to delete this project?")? { - fs::remove_dir_all(&tmp_dir)?; - println!("Deleted.") - } else { - println!("Saved as {tmp_dir:?}."); - } - - Ok(()) -} - -fn ask_y_n(question: &str) -> anyhow::Result { - println!("{question} (Y/n)"); - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - match input.to_lowercase().trim() { - "" => Ok(true), // default to yes if only enter is pressed - "y" => Ok(true), - "n" => Ok(false), - _ => ask_y_n(question), - } -} - -// modified from https://stackoverflow.com/a/65192210/15425442 -fn copy_dir_recursively(from: &Path, to: &Path) -> anyhow::Result<()> { - fs::create_dir_all(to)?; - - for entry in fs::read_dir(from)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - copy_dir_recursively(&path, &to.join(entry.file_name()))?; - } else { - fs::copy(path, to.join(entry.file_name()))?; - } - } - - Ok(()) + pub list_templates: bool, } diff --git a/src/main.rs b/src/main.rs index c90302d..1d7ce7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,14 @@ +use std::{ + borrow::Cow, + env, fs, io, + path::{Path, PathBuf}, + process::Command, +}; + use anyhow::bail; -use atmpt::Atmpt; +use atmpt::{templates::Templates, Atmpt}; +use chrono::Local; +use clap::Parser; use directories_next::ProjectDirs; fn main() -> anyhow::Result<()> { @@ -7,5 +16,84 @@ fn main() -> anyhow::Result<()> { bail!("Could not generate any directories for this OS!"); }; - Atmpt::parse_with(&dirs) + let args = Atmpt::parse(); + let req = args.required; + + let mut data_dir = Cow::Borrowed(dirs.data_dir()); + if let Some(new_dir) = args.template_dir { + *data_dir.to_mut() = PathBuf::from(new_dir); + }; + + if let Some(template) = req.template { + let Some(editor) = args.editor else { + bail!("No editor to use! Set your $VISUAL variable or pass a command to --editor"); + }; + + try_template(&template, &editor, args.delete, &data_dir)?; + } else if req.list_template_dir { + println!("{}", data_dir.display()); + } else { + println!("{}", Templates::try_from(data_dir.as_ref())?); + } + + Ok(()) +} + +fn try_template(template: &str, editor: &str, delete: bool, data_dir: &Path) -> anyhow::Result<()> { + let templates = Templates::try_from(data_dir)?; + let wanted_dir = templates.find(template)?; + + let time = Local::now().format("%Y_%m_%d-%H_%M_%S"); + let tmp_dir = env::temp_dir() + .join("atmpt") // store tmp projects in folder + .join(format!("{template}_{time}")); + copy_dir_recursively(wanted_dir, &tmp_dir)?; + + std::env::set_current_dir(&tmp_dir).expect("Could not change to temp directory!"); + Command::new(editor) + .arg(&tmp_dir) + .spawn()? + .wait() + .expect("Could not launch editor!"); + + if delete || ask_y_n("Would you like to delete this project?")? { + fs::remove_dir_all(&tmp_dir)?; + println!("Deleted.") + } else { + println!("Saved as {tmp_dir:?}."); + } + + Ok(()) +} + +fn ask_y_n(question: &str) -> anyhow::Result { + println!("{question} (Y/n)"); + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + + match input.to_lowercase().trim() { + "" => Ok(true), // default to yes if only enter is pressed + "y" => Ok(true), + "n" => Ok(false), + _ => ask_y_n(question), + } +} + +// modified from https://stackoverflow.com/a/65192210/15425442 +fn copy_dir_recursively(from: &Path, to: &Path) -> anyhow::Result<()> { + fs::create_dir_all(to)?; + + for entry in fs::read_dir(from)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + copy_dir_recursively(&path, &to.join(entry.file_name()))?; + } else { + fs::copy(path, to.join(entry.file_name()))?; + } + } + + Ok(()) } diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 0000000..3e16608 --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1,40 @@ +use std::{env, path::PathBuf}; + +use assert_cmd::Command; +use atmpt::{ALWAYS_DELETE_KEY, EDITOR_KEY, TEMPLATE_DIR_KEY}; + +const PROJECT_DIR: &str = env!("CARGO_MANIFEST_DIR"); + +fn cmd() -> Command { + env::set_var(EDITOR_KEY, "echo"); // exit on success + env::set_var(ALWAYS_DELETE_KEY, "true"); + + let templates = PathBuf::from_iter([PROJECT_DIR, "templates"]); + env::set_var(TEMPLATE_DIR_KEY, templates.to_string_lossy().as_ref()); + + Command::cargo_bin("atmpt").unwrap() +} + +// ======= Failures ======= + +#[test] +fn fail_on_conflicting_opts() { + cmd().args(["-l", "-d"]).assert().failure(); +} + +#[test] +fn fail_on_conflicting_opts_with_template() { + cmd().args(["cpp", "-l", "-d"]).assert().failure(); +} + +#[test] +fn incorrect_template() { + cmd().arg("_blahblah!").assert().failure(); +} + +// ======= Successes ======= + +#[test] +fn correct_template() { + cmd().arg("cpp").assert().success(); +}