diff --git a/Cargo.lock b/Cargo.lock index 249a273..57f9f4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.3.2" @@ -155,7 +164,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.27", ] [[package]] @@ -182,6 +191,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "tracing-test", ] [[package]] @@ -302,6 +312,15 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.5.0" @@ -425,6 +444,50 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.3.4", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -473,7 +536,7 @@ checksum = "401797fe7833d72109fedec6bfcbe67c0eed9b99772f26eb8afd261f0abc6fd3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.27", ] [[package]] @@ -529,6 +592,17 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.27" @@ -578,7 +652,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.27", ] [[package]] @@ -602,7 +676,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.27", ] [[package]] @@ -632,14 +706,41 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] +[[package]] +name = "tracing-test" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4" +dependencies = [ + "lazy_static", + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08" +dependencies = [ + "lazy_static", + "quote", + "syn 1.0.109", +] + [[package]] name = "unicode-ident" version = "1.0.11" diff --git a/Cargo.toml b/Cargo.toml index 0007d84..8aab6a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,8 @@ tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3.17" } clap = { version = "4.3.4", features = ["derive", "env"] } dotenv = { version = "0.15.0" } + +serde_yaml = {version = "*"} +serde = {version = "*", features = ["derive"]} + +tracing-test = "*" \ No newline at end of file diff --git a/crates/cuddle-release/Cargo.toml b/crates/cuddle-release/Cargo.toml index 94d1045..13590bd 100644 --- a/crates/cuddle-release/Cargo.toml +++ b/crates/cuddle-release/Cargo.toml @@ -10,5 +10,8 @@ tracing.workspace = true tracing-subscriber.workspace = true clap.workspace = true dotenv.workspace = true -serde_yaml = "0.9.25" -serde = { version = "1.0.177", features = ["derive"] } +serde_yaml.workspace = true +serde.workspace = true + +[dev-dependencies] +tracing-test = {workspace = true, features = ["no-env-filter"]} \ No newline at end of file diff --git a/crates/cuddle-release/src/command.rs b/crates/cuddle-release/src/command.rs new file mode 100644 index 0000000..c862efe --- /dev/null +++ b/crates/cuddle-release/src/command.rs @@ -0,0 +1,293 @@ +use std::{ + io::Read, + ops::Deref, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; + +use anyhow::Context; +use clap::{Parser, Subcommand}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::ui::{ConsoleUi, DynUi}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub struct Command { + /// token is the personal access token from gitea. + #[arg( + env = "CUDDLE_PLEASE_TOKEN", + long, + long_help = "token is the personal access token from gitea. It requires at least repository write access, it isn't required by default, but for most usecases the flow will fail without it", + global = true + )] + token: Option, + + /// Which repository to publish against. If not supplied remote url will be used. + #[arg(long, global = true)] + repo_url: Option, + + /// which source directory to use, if not set `std::env::current_dir` is used instead. + #[arg(long, global = true)] + source: Option, + + #[arg(long, global = true)] + config_stdin: bool, + + #[command(subcommand)] + commands: Option, + + #[clap(skip)] + ui: DynUi, + + #[clap(skip)] + stdin: Option anyhow::Result + Send + Sync + 'static>>>, +} + +impl Command { + pub fn new() -> Self { + let args = std::env::args(); + + Self::new_from_args_with_stdin(Some(ConsoleUi::default()), args, || { + let mut input = String::new(); + std::io::stdin().read_to_string(&mut input)?; + Ok(input) + }) + } + + pub fn new_from_args(ui: Option, i: I) -> Self + where + I: IntoIterator, + T: Into + Clone, + UIF: Into, + { + let mut s = Self::parse_from(i); + + if let Some(ui) = ui { + s.ui = ui.into(); + } + + s + } + + pub fn new_from_args_with_stdin(ui: Option, i: I, input: F) -> Self + where + I: IntoIterator, + T: Into + Clone, + F: Fn() -> anyhow::Result + Send + Sync + 'static, + UIF: Into, + { + let mut s = Self::parse_from(i); + + if let Some(ui) = ui { + s.ui = ui.into(); + } + s.stdin = Some(Arc::new(Mutex::new(input))); + + s + } + + fn get_config( + &self, + current_dir: &Path, + stdin: Option, + ) -> anyhow::Result { + let config = get_config(current_dir, stdin)?; + + Ok(config) + } + + pub fn execute(self, current_dir: Option<&Path>) -> anyhow::Result<()> { + // 1. Parse the current directory + let current_dir = get_current_path(current_dir, self.source.clone())?; + let stdin = if self.config_stdin { + if let Some(stdin_fn) = self.stdin.clone() { + let output = (stdin_fn.lock().unwrap().deref())(); + Some(output.unwrap()) + } else { + None + } + } else { + None + }; + + match &self.commands { + Some(Commands::Config { command }) => match command { + ConfigCommand::List {} => { + tracing::debug!("running command: config list"); + let _config = self.get_config(current_dir.as_path(), stdin)?; + + self.ui.write_str_ln(&format!("cuddle-config")); + } + }, + None => { + tracing::debug!("running bare command"); + // 2. Parse the cuddle.please.yaml let cuddle.please.yaml take precedence + // 2a. if not existing use default. + // 2b. if not in a git repo abort. (unless --no-vcs is turned added) + + // 3. Create gitea client and do a health check + // 4. Fetch git tags for the current repository + // 5. Fetch git commits since last git tag + + // 6. Slice commits since last git tag + + // 7. Create a versioning client + // 8. Parse conventional commits and determine next version + + // 9a. Check for open pr. + // 10a. If exists parse history, rebase from master and rewrite pr + + // 9b. check for release commit and release, if release exists continue + // 10b. create release + } + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Subcommand)] +enum Commands { + Config { + #[command(subcommand)] + command: ConfigCommand, + }, +} +#[derive(Subcommand, Debug, Clone)] +enum ConfigCommand { + List {}, +} + +fn get_current_path( + optional_current_dir: Option<&Path>, + optional_source_path: Option, +) -> anyhow::Result { + let path = optional_source_path + .or_else(|| optional_current_dir.map(|p| p.to_path_buf())) // fall back on current env from environment + .filter(|v| v.to_string_lossy() != "") // make sure we don't get empty values + //.and_then(|p| p.canonicalize().ok()) // Make sure we get the absolute path + .context("could not find current dir, pass --source as a replacement")?; + + if !path.exists() { + anyhow::bail!("path doesn't exist {}", path.display()); + } + + Ok(path) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PleaseConfig {} + +impl PleaseConfig { + fn merge(self, _config: PleaseConfig) -> Self { + self + } +} + +impl Default for PleaseConfig { + fn default() -> Self { + Self {} + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CuddleEmbeddedPleaseConfig { + please: PleaseConfig, +} + +impl From for PleaseConfig { + fn from(value: CuddleEmbeddedPleaseConfig) -> Self { + value.please + } +} +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CuddlePleaseConfig { + #[serde(flatten)] + please: PleaseConfig, +} +impl From for PleaseConfig { + fn from(value: CuddlePleaseConfig) -> Self { + value.please + } +} + +const CUDDLE_FILE_NAME: &'static str = "cuddle"; +const CUDDLE_CONFIG_FILE_NAME: &'static str = "cuddle.please"; +const YAML_EXTENSION: &'static str = "yaml"; + +fn get_config(current_dir: &Path, stdin: Option) -> anyhow::Result { + let current_cuddle_path = current_dir + .clone() + .join(format!("{CUDDLE_FILE_NAME}.{YAML_EXTENSION}")); + let current_cuddle_config_path = current_dir + .clone() + .join(format!("{CUDDLE_CONFIG_FILE_NAME}.{YAML_EXTENSION}")); + let mut please_config = PleaseConfig::default(); + + if let Some(config) = get_config_from_file::(current_cuddle_path) { + please_config = please_config.merge(config); + } + + if let Some(config) = get_config_from_file::(current_cuddle_config_path) { + please_config = please_config.merge(config); + } + + if let Some(input_config) = get_config_from_stdin::(stdin.as_ref()) { + please_config = please_config.merge(input_config); + } + + Ok(please_config) +} + +fn get_config_from_file<'d, T>(current_cuddle_path: PathBuf) -> Option +where + T: DeserializeOwned, + T: Into, +{ + match std::fs::File::open(¤t_cuddle_path) { + Ok(file) => match serde_yaml::from_reader::<_, T>(file) { + Ok(config) => { + return Some(config.into()); + } + Err(e) => { + tracing::debug!( + "{} doesn't contain a valid please config: {}", + ¤t_cuddle_path.display(), + e + ); + } + }, + Err(e) => { + tracing::debug!( + "did not find or was not allowed to read {}, error: {}", + ¤t_cuddle_path.display(), + e, + ); + } + } + + None +} + +fn get_config_from_stdin<'d, T>(stdin: Option<&'d String>) -> Option +where + T: Deserialize<'d>, + T: Into, +{ + match stdin { + Some(content) => match serde_yaml::from_str::<'d, T>(&content) { + Ok(config) => { + return Some(config.into()); + } + Err(e) => { + tracing::debug!("stdin doesn't contain a valid please config: {}", e); + } + }, + None => { + tracing::trace!("Stdin was not set continueing",); + } + } + + None +} diff --git a/crates/cuddle-release/src/lib.rs b/crates/cuddle-release/src/lib.rs new file mode 100644 index 0000000..14e82c0 --- /dev/null +++ b/crates/cuddle-release/src/lib.rs @@ -0,0 +1,2 @@ +pub mod command; +pub mod ui; diff --git a/crates/cuddle-release/src/main.rs b/crates/cuddle-release/src/main.rs index a7096ba..145acaa 100644 --- a/crates/cuddle-release/src/main.rs +++ b/crates/cuddle-release/src/main.rs @@ -1,78 +1,20 @@ -use std::{net::SocketAddr, path::PathBuf}; +pub mod command; +pub mod ui; -use anyhow::Context; -use clap::{Parser, Subcommand}; +use command::Command; +use ui::{ConsoleUi}; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); tracing_subscriber::fmt::init(); - let cli = Command::parse(); + let current_dir = std::env::current_dir().ok(); + let current_dir = current_dir.as_ref().map(|p| p.as_path()); - // 1. Parse the current directory - let current_dir = get_current_path(cli.source.clone())?; - let get_config = get_config(¤t_dir)?; + let _ui = ConsoleUi::new(); - // 2. Parse the cuddle.please.yaml let cuddle.please.yaml take precedence - // 2a. if not existing use default. - // 2b. if not in a git repo abort. (unless --no-vcs is turned added) - - // 3. Create gitea client and do a health check - // 4. Fetch git tags for the current repository - // 5. Fetch git commits since last git tag - - // 6. Slice commits since last git tag - - // 7. Create a versioning client - // 8. Parse conventional commits and determine next version - - // 9a. Check for open pr. - // 10a. If exists parse history, rebase from master and rewrite pr - - // 9b. check for release commit and release, if release exists continue - // 10b. create release + Command::new().execute(current_dir)?; Ok(()) } - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Command { - /// token is the personal access token from gitea. - #[arg( - env = "CUDDLE_PLEASE_TOKEN", - long, - long_help = "token is the personal access token from gitea. It requires at least repository write access, it isn't required by default, but for most usecases the flow will fail without it" - )] - token: Option, - - /// Which repository to publish against. If not supplied remote url will be used. - #[arg(long)] - repo_url: Option, - - /// which source directory to use, if not set `std::env::current_dir` is used instead. - #[arg(long)] - source: Option, -} - -fn get_current_path(optional_source_path: Option) -> anyhow::Result { - let path = optional_source_path - .or_else(|| std::env::current_dir().ok()) // fall back on current env from environment - .filter(|v| v.to_string_lossy() != "") // make sure we don't get empty values - .and_then(|p| p.canonicalize().ok()) // Make sure we get the absolute path - .context("could not find current dir, pass --source as a replacement")?; - - if !path.exists() { - anyhow::bail!("path doesn't exist {}", path.display()); - } - - Ok(path) -} -struct PleaseConfig {} - -const CUDDLE_FILE_NAME: &'static str = "cuddle"; -const CUDDLE_CONFIG_FILE_NAME: &'static str = "cuddle.please"; -const YAML_EXTENSIONS: Vec<&'static str> = vec!["yaml", "yml"]; - -fn get_config(current_dir: &PathBuf) -> anyhow::Result<()> {} diff --git a/crates/cuddle-release/src/ui.rs b/crates/cuddle-release/src/ui.rs new file mode 100644 index 0000000..743de98 --- /dev/null +++ b/crates/cuddle-release/src/ui.rs @@ -0,0 +1,53 @@ +pub trait Ui { + fn write_str(&self, content: &str); + fn write_err_str(&self, content: &str); + + fn write_str_ln(&self, content: &str); + fn write_err_str_ln(&self, content: &str); +} + +pub type DynUi = Box; + +impl Default for DynUi { + fn default() -> Self { + Box::new(ConsoleUi::default()) + } +} + +pub(crate) struct ConsoleUi {} + +impl ConsoleUi { + pub fn new() -> Self { + Self::default() + } +} + +impl Default for ConsoleUi { + fn default() -> Self { + Self {} + } +} + +impl From for DynUi { + fn from(value: ConsoleUi) -> Self { + Box::new(value) + } +} + +impl Ui for ConsoleUi { + fn write_str(&self, content: &str) { + print!("{}", content) + } + + fn write_err_str(&self, content: &str) { + eprint!("{}", content) + } + + fn write_str_ln(&self, content: &str) { + println!("{}", content) + } + + fn write_err_str_ln(&self, content: &str) { + eprintln!("{}", content) + } +} diff --git a/crates/cuddle-release/testdata/cuddle-embed/cuddle.yaml b/crates/cuddle-release/testdata/cuddle-embed/cuddle.yaml new file mode 100644 index 0000000..e69de29 diff --git a/crates/cuddle-release/testdata/cuddle-please/cuddle.please.yaml b/crates/cuddle-release/testdata/cuddle-please/cuddle.please.yaml new file mode 100644 index 0000000..e69de29 diff --git a/crates/cuddle-release/tests/common/mod.rs b/crates/cuddle-release/tests/common/mod.rs new file mode 100644 index 0000000..aeba0d9 --- /dev/null +++ b/crates/cuddle-release/tests/common/mod.rs @@ -0,0 +1,113 @@ +use cuddle_release::ui::{DynUi, Ui}; + +use std::{ + io::Write, + sync::{Arc, Mutex}, +}; + +struct BufferInner { + pub stdout: Vec, + pub stderr: Vec, +} + +impl BufferInner { + fn write_str(&mut self, content: &str) { + write!(&mut self.stdout, "{}", content).unwrap(); + } + + fn write_err_str(&mut self, content: &str) { + write!(&mut self.stderr, "{}", content).unwrap(); + } + + fn write_str_ln(&mut self, content: &str) { + writeln!(&mut self.stdout, "{}", content).unwrap(); + } + + fn write_err_str_ln(&mut self, content: &str) { + writeln!(&mut self.stderr, "{}", content).unwrap(); + } +} + +#[derive(Clone)] +pub struct BufferUi { + inner: Arc>, +} + +impl BufferUi { + pub fn get_stdout(&self) -> String { + let inner = self.inner.lock().unwrap(); + let output = std::str::from_utf8(&inner.stdout).unwrap(); + + output.to_string() + } + + pub fn get_stderr(&self) -> String { + let inner = self.inner.lock().unwrap(); + let output = std::str::from_utf8(&inner.stderr).unwrap(); + + output.to_string() + } + + pub fn get_output(&self) -> (String, String) { + let inner = self.inner.lock().unwrap(); + let stdout = std::str::from_utf8(&inner.stdout).unwrap(); + let stderr = std::str::from_utf8(&inner.stderr).unwrap(); + + (stdout.to_string(), stderr.to_string()) + } +} + +impl Ui for BufferUi { + fn write_str(&self, content: &str) { + let mut inner = self.inner.lock().unwrap(); + print!("{}", content); + inner.write_str(content) + } + + fn write_err_str(&self, content: &str) { + let mut inner = self.inner.lock().unwrap(); + eprint!("{}", content); + inner.write_err_str(content) + } + + fn write_str_ln(&self, content: &str) { + let mut inner = self.inner.lock().unwrap(); + println!("{}", content); + inner.write_str_ln(content) + } + + fn write_err_str_ln(&self, content: &str) { + let mut inner = self.inner.lock().unwrap(); + eprintln!("{}", content); + inner.write_err_str_ln(content) + } +} + +impl Default for BufferInner { + fn default() -> Self { + Self { + stdout: Vec::new(), + stderr: Vec::new(), + } + } +} + +impl Default for BufferUi { + fn default() -> Self { + Self { + inner: Arc::new(Mutex::new(BufferInner::default())), + } + } +} + +impl From for DynUi { + fn from(value: BufferUi) -> Self { + Box::new(value) + } +} + +impl From<&BufferUi> for DynUi { + fn from(value: &BufferUi) -> Self { + value.clone().into() + } +} diff --git a/crates/cuddle-release/tests/config.rs b/crates/cuddle-release/tests/config.rs new file mode 100644 index 0000000..c4705d4 --- /dev/null +++ b/crates/cuddle-release/tests/config.rs @@ -0,0 +1,83 @@ +pub mod common; + +use std::path::PathBuf; + +use common::BufferUi; +use cuddle_release::command::Command; +use tracing_test::traced_test; + +fn get_base_args<'a>() -> Vec<&'a str> { + vec!["cuddle-please", "config", "list"] +} + +fn assert_output(ui: &BufferUi, expected_stdout: &str, expected_stderr: &str) { + let (stdout, stderr) = ui.get_output(); + + assert_eq!(expected_stdout, &stdout); + assert_eq!(expected_stderr, &stderr); +} + +fn get_test_data_path(item: &str) -> PathBuf { + std::env::current_dir() + .ok() + .map(|p| p.join("testdata").join(item)) + .unwrap() +} + +#[test] +#[traced_test] +fn test_config_from_current_dir() { + let args = get_base_args(); + let ui = &BufferUi::default(); + let current_dir = get_test_data_path("cuddle-embed"); + + Command::new_from_args(Some(ui), args.into_iter()) + .execute(Some(¤t_dir)) + .unwrap(); + + assert_output(ui, "cuddle-config\n", ""); +} + +#[test] +#[traced_test] +fn test_config_from_source_dir() { + let mut args = get_base_args(); + let ui = &BufferUi::default(); + let current_dir = get_test_data_path("cuddle-embed"); + args.push("--source"); + args.push(current_dir.to_str().unwrap()); + + Command::new_from_args(Some(ui), args.into_iter()) + .execute(None) + .unwrap(); + + assert_output(ui, "cuddle-config\n", ""); +} + +#[test] +#[traced_test] +fn test_config_from_stdin() { + let mut args = get_base_args(); + let ui = &BufferUi::default(); + let current_dir = get_test_data_path("cuddle-embed"); + args.push("--source"); + args.push(current_dir.to_str().unwrap()); + args.push("--config-stdin"); + + Command::new_from_args_with_stdin(Some(ui), args.into_iter(), || Ok("please".into())) + .execute(None) + .unwrap(); + + assert_output(ui, "cuddle-config\n", ""); +} + +#[test] +#[traced_test] +fn test_config_fails_when_not_path_is_set() { + let args = get_base_args(); + let ui = &BufferUi::default(); + + let res = Command::new_from_args(Some(ui), args.into_iter()).execute(None); + + assert!(res.is_err()) +}