From f44f46bccb1b2103e2b9a2bb410fe8eba03bc1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sat, 29 Apr 2023 17:42:24 +0200 Subject: Rename new subcommand to add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- README.md | 6 +- src/cli.rs | 2 +- src/command/add_command.rs | 436 ++++++++++++++++++++++++++++ src/command/mod.rs | 4 +- src/command/new_command.rs | 436 ---------------------------- src/main.rs | 4 +- tests/add_command.rs | 323 +++++++++++++++++++++ tests/add_command_creates_default_header.rs | 87 ++++++ tests/add_command_editor.rs | 112 +++++++ tests/common.rs | 4 +- tests/generate_command.rs | 2 +- tests/new_command.rs | 323 --------------------- tests/new_command_creates_default_header.rs | 87 ------ tests/new_command_editor.rs | 112 ------- tests/release_command.rs | 6 +- tests/verify_metadata_command.rs | 2 +- 16 files changed, 973 insertions(+), 973 deletions(-) create mode 100644 src/command/add_command.rs delete mode 100644 src/command/new_command.rs create mode 100644 tests/add_command.rs create mode 100644 tests/add_command_creates_default_header.rs create mode 100644 tests/add_command_editor.rs delete mode 100644 tests/new_command.rs delete mode 100644 tests/new_command_creates_default_header.rs delete mode 100644 tests/new_command_editor.rs diff --git a/README.md b/README.md index 939b7e0..e7496c2 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ seamlessly integrate with the generated one. It works in the following way: - Everytime you add/change/fix something and want to document it, you create a - new changelog entry with `cargo changelog new` + new changelog entry with `cargo changelog add` - Then, when a new version is released, you run `cargo changelog generate ` to move all unreleased changes to either the next patch/minor/major version @@ -36,9 +36,9 @@ It works in the following way: Here's how they work individually: -### cargo changelog new +### cargo changelog add -`cargo changelog new` generates a new changelog file in the unreleased +`cargo changelog add` generates a new changelog file in the unreleased changelog directory. Per default that is `.changelogs/unreleased`. If interactive mode is enabled, which it is per-default, then you will be prompted to fill in the fields of the changelog as well as a larger free-form diff --git a/src/cli.rs b/src/cli.rs index 10e9405..a0f01cc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,7 +32,7 @@ pub enum Command { Init, /// Create a new changelog fragment - New { + Add { #[clap(short, long, action = clap::ArgAction::Set, default_value_t = true)] interactive: bool, diff --git a/src/command/add_command.rs b/src/command/add_command.rs new file mode 100644 index 0000000..4e2941d --- /dev/null +++ b/src/command/add_command.rs @@ -0,0 +1,436 @@ +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; + +use dialoguer::Confirm; +use dialoguer::Input; +use dialoguer::Select; + +use crate::cli::TextProvider; +use crate::cli::KV; +use crate::config::Configuration; +use crate::config::GitSetting; +use crate::error::Error; +use crate::error::FragmentError; +use crate::error::InteractiveError; +use crate::format::Format; +use crate::fragment::Crawler; +use crate::fragment::FragmentData; +use crate::fragment::FragmentDataDesc; +use crate::fragment::FragmentDataType; +use crate::fragment::FragmentDataTypeDefinite; + +#[derive(Debug, typed_builder::TypedBuilder)] +pub struct AddCommand { + interactive: bool, + edit: bool, + format: Format, + set: Vec, + text: Option, + git: Option, +} + +impl crate::command::Command for AddCommand { + fn execute(self, workdir: &Path, config: &Configuration) -> Result<(), Error> { + let unreleased_dir_path = ensure_fragment_dir(workdir, config)?; + + let new_file_path = { + let new_file_name = format!( + "{ts}.md", + ts = { + // We cannot use the well-known formats here, because cargo cannot package + // filenames with ":" in it, but the well-known formats contain this character. + // Hence we have to use our own. + let fragment_file_timestamp_format = time::macros::format_description!( + "[year]-[month]-[day]T[hour]_[minute]_[second]_[subsecond]" + ); + time::OffsetDateTime::now_utc().format(&fragment_file_timestamp_format)? + }, + ); + unreleased_dir_path.join(new_file_name) + }; + + let mut file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .append(false) + .open(&new_file_path)?; + + let mut fragment = crate::fragment::Fragment::empty(); + + if let Some(text_provider) = self.text.as_ref() { + let text = text_provider.read()?; + fragment.set_text(text); + } + + // Fill the fragment header with data + *fragment.header_mut() = config + .header_fields() + .iter() + .filter_map(|(key, data_desc)| { + let cli_set: Option = match self + .set + .iter() + .find(|kv| kv.key() == key) + .map(KV::value) + .map(|val| FragmentData::parse(val)) + { + Some(Ok(val)) => Some(val), + Some(Err(e)) => return Some(Err(e)), + None => None, + }; + let crawler = data_desc.crawler(); + let default_value = data_desc.default_value(); + + // if there is a default value, but its type is not correct, fail + if let Some(default) = default_value.as_ref() { + if !data_desc.fragment_type().matches(default) { + return Some(Err(FragmentError::DataType { + exp: data_desc.fragment_type().type_name(), + recv: default.type_name().to_string(), + field_name: key.to_string(), + })); + } + } + + // if there is a CLI provided value, but its type is not correct, fail + if let Some(clival) = cli_set.as_ref() { + if !data_desc.fragment_type().matches(clival) { + return Some(Err(FragmentError::DataType { + exp: data_desc.fragment_type().type_name(), + recv: clival.type_name().to_string(), + field_name: key.to_string(), + })); + } + } + + match (default_value, cli_set, crawler) { + (Some(default), None, None) => { + if self.interactive { + interactive_edit(key, default, data_desc) + .map_err(FragmentError::from) + .transpose() + } else { + Some(Ok((key.to_string(), default.clone()))) + } + } + + (_, Some(clival), _) => { + if self.interactive { + interactive_edit(key, &clival, data_desc) + .map_err(FragmentError::from) + .transpose() + } else { + Some(Ok((key.to_string(), clival))) + } + } + + (_, _, Some(crawler)) => { + let crawled_value = match crawl_with_crawler( + crawler, + key, + workdir, + data_desc.fragment_type(), + ) { + Err(e) => return Some(Err(e)), + Ok(val) => val, + }; + + if !data_desc.fragment_type().matches(&crawled_value) { + return Some(Err(FragmentError::DataType { + exp: data_desc.fragment_type().type_name(), + recv: crawled_value.type_name().to_string(), + field_name: key.to_string(), + })); + } + + Some(Ok((key.to_string(), crawled_value))) + } + + (None, None, None) => { + if data_desc.required() { + if self.interactive { + interactive_provide(key, data_desc) + .map_err(FragmentError::from) + .transpose() + } else { + Some(Err(FragmentError::RequiredValueMissing(key.to_string()))) + } + } else { + None + } + } + } + }) + .collect::, _>>() + .map_err(|e| Error::FragmentError(e, new_file_path.to_path_buf()))?; + + fragment + .write_to(&mut file, self.format) + .map_err(|e| Error::FragmentError(e, new_file_path.to_path_buf()))?; + file.sync_all()?; + drop(file); + + if self.edit { + let mut editor_command = get_editor_command()?; + let std::process::Output { status, .. } = editor_command + .arg(&new_file_path) + .stderr(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .output()?; + + if status.success() { + log::info!("Successfully edited"); + } else { + log::error!("Failure editing {}", new_file_path.display()); + } + } + + match self.git.as_ref().or_else(|| config.git().as_ref()) { + Some(GitSetting::Add) => { + // We use the simple approach here and use std::command::Command for calling git + Command::new("git") + .arg("add") + .arg(&new_file_path) + .stderr(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .output()?; + } + Some(GitSetting::Commit) => { + Command::new("git") + .arg("add") + .arg(&new_file_path) + .stderr(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .output()?; + + let mut commit_cmd = Command::new("git"); + commit_cmd.arg("commit").arg(&new_file_path); + + if let Some(message) = config.git_commit_message().as_ref() { + commit_cmd.arg("--message").arg(message); + } + + if config.git_commit_signoff() { + commit_cmd.arg("--signoff"); + } + + commit_cmd + .stderr(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .output()?; + } + None => {} + } + + Ok(()) + } +} + +fn ensure_fragment_dir(workdir: &Path, config: &Configuration) -> Result { + let unreleased_dir_path = workdir + .join(config.fragment_dir()) + .join(crate::consts::UNRELEASED_DIR_NAME); + std::fs::create_dir_all(&unreleased_dir_path)?; + Ok(unreleased_dir_path) +} + +fn get_editor_command() -> Result { + let editor = match std::env::var("EDITOR") { + Ok(editor) => editor, + Err(std::env::VarError::NotPresent) => match std::env::var("VISUAL") { + Ok(editor) => editor, + Err(std::env::VarError::NotPresent) => return Err(Error::EditorEnvNotSet), + Err(std::env::VarError::NotUnicode(_)) => { + return Err(Error::EnvNotUnicode("VISUAL".to_string())) + } + }, + Err(std::env::VarError::NotUnicode(_)) => { + return Err(Error::EnvNotUnicode("EDITOR".to_string())) + } + }; + + Ok(Command::new(editor)) +} + +/// Ask interactively whether these values are okay or should be changed +fn interactive_edit( + key: &str, + value: &FragmentData, + value_desc: &FragmentDataDesc, +) -> Result, InteractiveError> { + let prompt = format!("Edit '{key}' = '{data}' ({type})?", + key = key, + data = value.display(), + type = value.type_name()); + + let confirmed = dialoguer::Confirm::new() + .default(true) + .show_default(true) + .with_prompt(prompt) + .interact_opt() + .map_err(InteractiveError::from)?; + + match confirmed { + None => Err(InteractiveError::Interrupted), + Some(true) => Ok(Some((key.to_string(), value.clone()))), + Some(false) => interactive_provide(key, value_desc), + } +} + +/// Let the user provide a value matching the description interactively +fn interactive_provide( + key: &str, + desc: &FragmentDataDesc, +) -> Result, InteractiveError> { + match desc.fragment_type() { + FragmentDataType::Ty(FragmentDataTypeDefinite::Bool) => { + let mut dialoguer = Confirm::new(); + dialoguer.with_prompt(format!("'{key}'?")); + if let Some(data) = desc.default_value() { + if let FragmentData::Bool(b) = data { + dialoguer.default(*b); + } else { + return Err(InteractiveError::TypeError( + desc.fragment_type().clone(), + data.clone(), + )); + } + } + + let value = if desc.required() { + dialoguer.interact().map_err(InteractiveError::from)? + } else { + let value = dialoguer.interact_opt().map_err(InteractiveError::from)?; + match value { + None => return Ok(None), + Some(val) => val, + } + }; + + Ok(Some((key.to_string(), FragmentData::Bool(value)))) + } + FragmentDataType::Ty(FragmentDataTypeDefinite::Int) => { + let mut dialoguer = Input::::new(); + dialoguer.with_prompt(format!("Enter a number for '{key}'")); + + if let Some(data) = desc.default_value() { + if let FragmentData::Int(i) = data { + dialoguer.default(*i); + } else { + return Err(InteractiveError::TypeError( + desc.fragment_type().clone(), + data.clone(), + )); + } + } + + let value = dialoguer.interact_text().map_err(InteractiveError::from)?; + Ok(Some((key.to_string(), FragmentData::Int(value)))) + } + FragmentDataType::Ty(FragmentDataTypeDefinite::Str) => { + let mut dialoguer = Input::::new(); + dialoguer.with_prompt(format!("Enter a text for '{key}'")); + + if let Some(data) = desc.default_value() { + if let FragmentData::Str(s) = data { + dialoguer.default(s.to_string()); + } else { + return Err(InteractiveError::TypeError( + desc.fragment_type().clone(), + data.clone(), + )); + } + } + + let value = dialoguer.interact_text().map_err(InteractiveError::from)?; + Ok(Some((key.to_string(), FragmentData::Str(value)))) + } + FragmentDataType::OneOf(possible_values) => { + let mut dialoguer = Select::new(); + dialoguer.items(possible_values); + dialoguer.with_prompt("Select one"); + + if let Some(default_value) = desc.default_value() { + if let FragmentData::Str(default_value) = default_value { + if let Some(default_idx) = possible_values + .iter() + .enumerate() + .find(|(_, elmt)| *elmt == default_value) + .map(|(i, _)| i) + { + dialoguer.default(default_idx); + } + } else { + return Err(InteractiveError::TypeError( + desc.fragment_type().clone(), + default_value.clone(), + )); + } + } + + let value_idx = dialoguer.interact().map_err(InteractiveError::from)?; + let value = possible_values + .get(value_idx) + .ok_or(InteractiveError::IndexError( + value_idx, + possible_values.len(), + ))?; + Ok(Some(( + key.to_string(), + FragmentData::Str(value.to_string()), + ))) + } + } +} + +fn crawl_with_crawler( + crawler: &Crawler, + field_name: &str, + workdir: &Path, + expected_type: &FragmentDataType, +) -> Result { + let (command_str, mut command) = match crawler { + Crawler::Path(path) => (path.display().to_string(), Command::new(workdir.join(path))), + Crawler::Command(s) => { + let mut cmd = comma::parse_command(s) + .ok_or_else(|| FragmentError::NoValidCommand(s.to_string()))?; + let binary = cmd.remove(0); + let mut command = Command::new(binary); + command.args(cmd); + (s.to_string(), command) + } + }; + + let std::process::Output { status, stdout, .. } = command + .stderr(std::process::Stdio::inherit()) + .env("CARGO_CHANGELOG_CRAWLER_FIELD_NAME", field_name) + .env( + "CARGO_CHANGELOG_CRAWLER_FIELD_TYPE", + expected_type.type_name(), + ) + .output() + .map_err(FragmentError::from)?; + + if status.success() { + log::info!("Executed crawl successfully"); + let out = String::from_utf8(stdout) + .map_err(|e| FragmentError::NoUtf8Output(command_str, e))? + .trim() + .to_string(); + log::info!("crawled = '{}'", out); + let data = FragmentData::parse(&out)?; + if expected_type.matches(&data) { + Ok(data) + } else { + Err(FragmentError::DataType { + exp: expected_type.type_name(), + recv: data.type_name().to_string(), + field_name: field_name.to_string(), + }) + } + } else { + Err(FragmentError::CommandNoSuccess(command_str)) + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs index e9537fd..0513dfe 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -4,8 +4,8 @@ use crate::config::Configuration; mod common; -mod new_command; -pub use self::new_command::NewCommand; +mod add_command; +pub use self::add_command::AddCommand; mod generate_command; pub use self::generate_command::GenerateCommand; diff --git a/src/command/new_command.rs b/src/command/new_command.rs deleted file mode 100644 index 8c8192e..0000000 --- a/src/command/new_command.rs +++ /dev/null @@ -1,436 +0,0 @@ -use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; - -use dialoguer::Confirm; -use dialoguer::Input; -use dialoguer::Select; - -use crate::cli::TextProvider; -use crate::cli::KV; -use crate::config::Configuration; -use crate::config::GitSetting; -use crate::error::Error; -use crate::error::FragmentError; -use crate::error::InteractiveError; -use crate::format::Format; -use crate::fragment::Crawler; -use crate::fragment::FragmentData; -use crate::fragment::FragmentDataDesc; -use crate::fragment::FragmentDataType; -use crate::fragment::FragmentDataTypeDefinite; - -#[derive(Debug, typed_builder::TypedBuilder)] -pub struct NewCommand { - interactive: bool, - edit: bool, - format: Format, - set: Vec, - text: Option, - git: Option, -} - -impl crate::command::Command for NewCommand { - fn execute(self, workdir: &Path, config: &Configuration) -> Result<(), Error> { - let unreleased_dir_path = ensure_fragment_dir(workdir, config)?; - - let new_file_path = { - let new_file_name = format!( - "{ts}.md", - ts = { - // We cannot use the well-known formats here, because cargo cannot package - // filenames with ":" in it, but the well-known formats contain this character. - // Hence we have to use our own. - let fragment_file_timestamp_format = time::macros::format_description!( - "[year]-[month]-[day]T[hour]_[minute]_[second]_[subsecond]" - ); - time::OffsetDateTime::now_utc().format(&fragment_file_timestamp_format)? - }, - ); - unreleased_dir_path.join(new_file_name) - }; - - let mut file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .append(false) - .open(&new_file_path)?; - - let mut fragment = crate::fragment::Fragment::empty(); - - if let Some(text_provider) = self.text.as_ref() { - let text = text_provider.read()?; - fragment.set_text(text); - } - - // Fill the fragment header with data - *fragment.header_mut() = config - .header_fields() - .iter() - .filter_map(|(key, data_desc)| { - let cli_set: Option = match self - .set - .iter() - .find(|kv| kv.key() == key) - .map(KV::value) - .map(|val| FragmentData::parse(val)) - { - Some(Ok(val)) => Some(val), - Some(Err(e)) => return Some(Err(e)), - None => None, - }; - let crawler = data_desc.crawler(); - let default_value = data_desc.default_value(); - - // if there is a default value, but its type is not correct, fail - if let Some(default) = default_value.as_ref() { - if !data_desc.fragment_type().matches(default) { - return Some(Err(FragmentError::DataType { - exp: data_desc.fragment_type().type_name(), - recv: default.type_name().to_string(), - field_name: key.to_string(), - })); - } - } - - // if there is a CLI provided value, but its type is not correct, fail - if let Some(clival) = cli_set.as_ref() { - if !data_desc.fragment_type().matches(clival) { - return Some(Err(FragmentError::DataType { - exp: data_desc.fragment_type().type_name(), - recv: clival.type_name().to_string(), - field_name: key.to_string(), - })); - } - } - - match (default_value, cli_set, crawler) { - (Some(default), None, None) => { - if self.interactive { - interactive_edit(key, default, data_desc) - .map_err(FragmentError::from) - .transpose() - } else { - Some(Ok((key.to_string(), default.clone()))) - } - } - - (_, Some(clival), _) => { - if self.interactive { - interactive_edit(key, &clival, data_desc) - .map_err(FragmentError::from) - .transpose() - } else { - Some(Ok((key.to_string(), clival))) - } - } - - (_, _, Some(crawler)) => { - let crawled_value = match crawl_with_crawler( - crawler, - key, - workdir, - data_desc.fragment_type(), - ) { - Err(e) => return Some(Err(e)), - Ok(val) => val, - }; - - if !data_desc.fragment_type().matches(&crawled_value) { - return Some(Err(FragmentError::DataType { - exp: data_desc.fragment_type().type_name(), - recv: crawled_value.type_name().to_string(), - field_name: key.to_string(), - })); - } - - Some(Ok((key.to_string(), crawled_value))) - } - - (None, None, None) => { - if data_desc.required() { - if self.interactive { - interactive_provide(key, data_desc) - .map_err(FragmentError::from) - .transpose() - } else { - Some(Err(FragmentError::RequiredValueMissing(key.to_string()))) - } - } else { - None - } - } - } - }) - .collect::, _>>() - .map_err(|e| Error::FragmentError(e, new_file_path.to_path_buf()))?; - - fragment - .write_to(&mut file, self.format) - .map_err(|e| Error::FragmentError(e, new_file_path.to_path_buf()))?; - file.sync_all()?; - drop(file); - - if self.edit { - let mut editor_command = get_editor_command()?; - let std::process::Output { status, .. } = editor_command - .arg(&new_file_path) - .stderr(std::process::Stdio::inherit()) - .stdout(std::process::Stdio::inherit()) - .output()?; - - if status.success() { - log::info!("Successfully edited"); - } else { - log::error!("Failure editing {}", new_file_path.display()); - } - } - - match self.git.as_ref().or_else(|| config.git().as_ref()) { - Some(GitSetting::Add) => { - // We use the simple approach here and use std::command::Command for calling git - Command::new("git") - .arg("add") - .arg(&new_file_path) - .stderr(std::process::Stdio::inherit()) - .stdout(std::process::Stdio::inherit()) - .output()?; - } - Some(GitSetting::Commit) => { - Command::new("git") - .arg("add") - .arg(&new_file_path) - .stderr(std::process::Stdio::inherit()) - .stdout(std::process::Stdio::inherit()) - .output()?; - - let mut commit_cmd = Command::new("git"); - commit_cmd.arg("commit").arg(&new_file_path); - - if let Some(message) = config.git_commit_message().as_ref() { - commit_cmd.arg("--message").arg(message); - } - - if config.git_commit_signoff() { - commit_cmd.arg("--signoff"); - } - - commit_cmd - .stderr(std::process::Stdio::inherit()) - .stdout(std::process::Stdio::inherit()) - .output()?; - } - None => {} - } - - Ok(()) - } -} - -fn ensure_fragment_dir(workdir: &Path, config: &Configuration) -> Result { - let unreleased_dir_path = workdir - .join(config.fragment_dir()) - .join(crate::consts::UNRELEASED_DIR_NAME); - std::fs::create_dir_all(&unreleased_dir_path)?; - Ok(unreleased_dir_path) -} - -fn get_editor_command() -> Result { - let editor = match std::env::var("EDITOR") { - Ok(editor) => editor, - Err(std::env::VarError::NotPresent) => match std::env::var("VISUAL") { - Ok(editor) => editor, - Err(std::env::VarError::NotPresent) => return Err(Error::EditorEnvNotSet), - Err(std::env::VarError::NotUnicode(_)) => { - return Err(Error::EnvNotUnicode("VISUAL".to_string())) - } - }, - Err(std::env::VarError::NotUnicode(_)) => { - return Err(Error::EnvNotUnicode("EDITOR".to_string())) - } - }; - - Ok(Command::new(editor)) -} - -/// Ask interactively whether these values are okay or should be changed -fn interactive_edit( - key: &str, - value: &FragmentData, - value_desc: &FragmentDataDesc, -) -> Result, InteractiveError> { - let prompt = format!("Edit '{key}' = '{data}' ({type})?", - key = key, - data = value.display(), - type = value.type_name()); - - let confirmed = dialoguer::Confirm::new() - .default(true) - .show_default(true) - .with_prompt(prompt) - .interact_opt() - .map_err(InteractiveError::from)?; - - match confirmed { - None => Err(InteractiveError::Interrupted), - Some(true) => Ok(Some((key.to_string(), value.clone()))), - Some(false) => interactive_provide(key, value_desc), - } -} - -/// Let the user provide a value matching the description interactively -fn interactive_provide( - key: &str, - desc: &FragmentDataDesc, -) -> Result, InteractiveError> { - match desc.fragment_type() { - FragmentDataType::Ty(FragmentDataTypeDefinite::Bool) => { - let mut dialoguer = Confirm::new(); - dialoguer.with_prompt(format!("'{key}'?")); - if let Some(data) = desc.default_value() { - if let FragmentData::Bool(b) = data { - dialoguer.default(*b); - } else { - return Err(InteractiveError::TypeError( - desc.fragment_type().clone(), - data.clone(), - )); - } - } - - let value = if desc.required() { - dialoguer.interact().map_err(InteractiveError::from)? - } else { - let value = dialoguer.interact_opt().map_err(InteractiveError::from)?; - match value { - None => return Ok(None), - Some(val) => val, - } - }; - - Ok(Some((key.to_string(), FragmentData::Bool(value)))) - } - FragmentDataType::Ty(FragmentDataTypeDefinite::Int) => { - let mut dialoguer = Input::::new(); - dialoguer.with_prompt(format!("Enter a number for '{key}'")); - - if let Some(data) = desc.default_value() { - if let FragmentData::Int(i) = data { - dialoguer.default(*i); - } else { - return Err(InteractiveError::TypeError( - desc.fragment_type().clone(), - data.clone(), - )); - } - } - - let value = dialoguer.interact_text().map_err(InteractiveError::from)?; - Ok(Some((key.to_string(), FragmentData::Int(value)))) - } - FragmentDataType::Ty(FragmentDataTypeDefinite::Str) => { - let mut dialoguer = Input::::new(); - dialoguer.with_prompt(format!("Enter a text for '{key}'")); - - if let Some(data) = desc.default_value() { - if let FragmentData::Str(s) = data { - dialoguer.default(s.to_string()); - } else { - return Err(InteractiveError::TypeError( - desc.fragment_type().clone(), - data.clone(), - )); - } - } - - let value = dialoguer.interact_text().map_err(InteractiveError::from)?; - Ok(Some((key.to_string(), FragmentData::Str(value)))) - } - FragmentDataType::OneOf(possible_values) => { - let mut dialoguer = Select::new(); - dialoguer.items(possible_values); - dialoguer.with_prompt("Select one"); - - if let Some(default_value) = desc.default_value() { - if let FragmentData::Str(default_value) = default_value { - if let Some(default_idx) = possible_values - .iter() - .enumerate() - .find(|(_, elmt)| *elmt == default_value) - .map(|(i, _)| i) - { - dialoguer.default(default_idx); - } - } else { - return Err(InteractiveError::TypeError( - desc.fragment_type().clone(), - default_value.clone(), - )); - } - } - - let value_idx = dialoguer.interact().map_err(InteractiveError::from)?; - let value = possible_values - .get(value_idx) - .ok_or(InteractiveError::IndexError( - value_idx, - possible_values.len(), - ))?; - Ok(Some(( - key.to_string(), - FragmentData::Str(value.to_string()), - ))) - } - } -} - -fn crawl_with_crawler( - crawler: &Crawler, - field_name: &str, - workdir: &Path, - expected_type: &FragmentDataType, -) -> Result { - let (command_str, mut command) = match crawler { - Crawler::Path(path) => (path.display().to_string(), Command::new(workdir.join(path))), - Crawler::Command(s) => { - let mut cmd = comma::parse_command(s) - .ok_or_else(|| FragmentError::NoValidCommand(s.to_string()))?; - let binary = cmd.remove(0); - let mut command = Command::new(binary); - command.args(cmd); - (s.to_string(), command) - } - }; - - let std::process::Output { status, stdout, .. } = command - .stderr(std::process::Stdio::inherit()) - .env("CARGO_CHANGELOG_CRAWLER_FIELD_NAME", field_name) - .env( - "CARGO_CHANGELOG_CRAWLER_FIELD_TYPE", - expected_type.type_name(), - ) - .output() - .map_err(FragmentError::from)?; - - if status.success() { - log::info!("Executed crawl successfully"); - let out = String::from_utf8(stdout) - .map_err(|e| FragmentError::NoUtf8Output(command_str, e))? - .trim() - .to_string(); - log::info!("crawled = '{}'", out); - let data = FragmentData::parse(&out)?; - if expected_type.matches(&data) { - Ok(data) - } else { - Err(FragmentError::DataType { - exp: expected_type.type_name(), - recv: data.type_name().to_string(), - field_name: field_name.to_string(), - }) - } - } else { - Err(FragmentError::CommandNoSuccess(command_str)) - } -} diff --git a/src/main.rs b/src/main.rs index 162c610..83eafbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,14 +56,14 @@ fn main() -> miette::Result<()> { match args.command { Command::Init => unreachable!(), // reached above - Command::New { + Command::Add { interactive, edit, format, read, set, git, - } => crate::command::NewCommand::builder() + } => crate::command::AddCommand::builder() .interactive(interactive) .edit(edit) .format(format) diff --git a/tests/add_command.rs b/tests/add_command.rs new file mode 100644 index 0000000..4bc2501 --- /dev/null +++ b/tests/add_command.rs @@ -0,0 +1,323 @@ +use std::io::Write; + +mod common; + +#[test] +fn add_command_creates_toml_file() { + let temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog") + .tempdir() + .unwrap(); + self::common::init_git(temp_dir.path()); + self::common::init_cargo_changelog(temp_dir.path()); + + self::common::cargo_changelog_add(temp_dir.path()) + .args([ + "--format=toml", + "--set", + "issue=123", + "--set", + "subject=This is some text", + "--set", + "type=Bugfix", + ]) + .assert() + .success(); + + let unreleased_dir = temp_dir.path().join(".changelogs").join("unreleased"); + if !unreleased_dir.exists() { + panic!("Unreleased directory does not exist"); + } + + let files = std::fs::read_dir(&unreleased_dir) + .unwrap() + .into_iter() + .collect::>(); + assert_eq!( + files.len(), + 2, + "Expected 2 entries in unreleased directory, found {}: {:?}", + files.len(), + files + ); + + let new_fragment_file = files + .into_iter() + .find(|rde| match rde { + Ok(de) => !de.path().ends_with(".gitkeep"), + Err(_) => true, + }) + .unwrap() + .unwrap(); + { + let ft = new_fragment_file.file_type().unwrap(); + assert!( + ft.is_file(), + "Expected {} to be a file, is {:?}", + new_fragment_file.path().display(), + ft + ); + } + + let new_fragment_file_contents = std::fs::read_to_string(new_fragment_file.path()).unwrap(); + let toml_header = new_fragment_file_contents + .lines() + .skip(1) + .take_while(|line| *line != "+++") + .collect::>() + .join("\n"); + + let toml = toml::from_str::(&toml_header); + assert!( + toml.is_ok(), + "Failed to parse fragment file: {:?}", + toml.unwrap_err() + ); +} + +#[test] +fn add_command_creates_unreleased_gitkeep() { + let temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog") + .tempdir() + .unwrap(); + self::common::init_git(temp_dir.path()); + self::common::init_cargo_changelog(temp_dir.path()); + + let unreleased_gitkeep_path = temp_dir + .path() + .join(".changelogs") + .join("unreleased") + .join(".gitkeep"); + if !unreleased_gitkeep_path.exists() { + panic!("unreleased gitkeep file does not exist"); + } + if !unreleased_gitkeep_path.is_file() { + panic!("unreleased gitkeep file is not a file"); + } +} + +#[test] +fn add_command_with_text_creates_toml_with_text_from_stdin() { + let temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog") + .tempdir() + .unwrap(); + self::common::init_git(temp_dir.path()); + self::common::init_cargo_changelog(temp_dir.path()); + + let test_text = "This is a test text"; + { + let text_temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog-new-test-text") + .tempdir() + .unwrap(); + let path = text_temp_dir.path().join("text_file.txt"); + let mut file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .append(false) + .open(&path) + .unwrap(); + + write!(file, "{test_text}").unwrap(); + file.sync_all().unwrap(); + drop(file); // make sure we close the handle + + self::common::cargo_changelog_add(temp_dir.path()) + .args([ + "--format=toml", + "--set", + "issue=123", + "--set", + "subject='This is some text'", + "--set", + "type=Bugfix", + "--read=-", // read text from STDIN + ]) + .pipe_stdin(path) + .unwrap() + .assert() + .success(); + } + + let fragment_file = std::fs::read_dir(temp_dir.path().join(".changelogs").join("unreleased")) + .unwrap() + .into_iter() + .find(|rde| match rde { + Ok(de) => !de.path().ends_with(".gitkeep"), + Err(_) => true, + }) + .unwrap() + .unwrap(); + + let new_fragment_file_contents = std::fs::read_to_string(fragment_file.path()).unwrap(); + let contents = new_fragment_file_contents + .lines() + .skip(1) + .skip_while(|line| *line != "+++") + .skip(1) + .collect::>() + .join("\n"); + + assert_eq!(contents, test_text); +} + +#[test] +fn add_command_with_text_creates_toml_with_text_from_file() { + let temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog") + .tempdir() + .unwrap(); + self::common::init_git(temp_dir.path()); + self::common::init_cargo_changelog(temp_dir.path()); + + let test_text = "This is a test text"; + { + let text_temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog-new-test-text") + .tempdir() + .unwrap(); + let path = text_temp_dir.path().join("text_file.txt"); + let mut file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .append(false) + .open(&path) + .unwrap(); + + write!(file, "{test_text}").unwrap(); + file.sync_all().unwrap(); + drop(file); // make sure we close the handle + + self::common::cargo_changelog_add(temp_dir.path()) + .args([ + "--format=toml", + "--set", + "issue=123", + "--set", + "subject='This is some text'", + "--set", + "type=Bugfix", + // read text from PATH + "--read", + &path.display().to_string(), + ]) + .pipe_stdin(path) + .unwrap() + .assert() + .success(); + } + + let fragment_file = std::fs::read_dir(temp_dir.path().join(".changelogs").join("unreleased")) + .unwrap() + .into_iter() + .find(|rde| match rde { + Ok(de) => !de.path().ends_with(".gitkeep"), + Err(_) => true, + }) + .unwrap() + .unwrap(); + + let new_fragment_file_contents = std::fs::read_to_string(fragment_file.path()).unwrap(); + let contents = new_fragment_file_contents + .lines() + .skip(1) + .skip_while(|line| *line != "+++") + .skip(1) + .collect::>() + .join("\n"); + + assert_eq!(contents, test_text); +} + +#[test] +fn add_command_creates_toml_header() { + let temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog") + .tempdir() + .unwrap(); + self::common::init_git(temp_dir.path()); + self::common::init_cargo_changelog(temp_dir.path()); + + self::common::cargo_changelog_add(temp_dir.path()) + .args([ + "--format=toml", + "--set", + "issue=123", + "--set", + "subject='This is some text'", + "--set", + "type=Bugfix", + ]) + .assert() + .success(); + + let unreleased_dir = temp_dir.path().join(".changelogs").join("unreleased"); + + let new_fragment_file = std::fs::read_dir(unreleased_dir) + .unwrap() + .into_iter() + .find(|rde| match rde { + Ok(de) => !de.path().ends_with(".gitkeep"), + Err(_) => true, + }) + .unwrap() + .unwrap(); + + let new_fragment_file_contents = std::fs::read_to_string(new_fragment_file.path()).unwrap(); + let toml_header = new_fragment_file_contents + .lines() + .skip(1) + .take_while(|line| *line != "+++") + .collect::>() + .join("\n"); + + let toml = toml::from_str::(&toml_header); + assert!( + toml.is_ok(), + "Failed to parse fragment file: {:?}", + toml.unwrap_err() + ); +} + +#[test] +fn add_command_cannot_create_nonexistent_oneof() { + let temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog") + .tempdir() + .unwrap(); + self::common::init_git(temp_dir.path()); + self::common::init_cargo_changelog(temp_dir.path()); + + { + // Write some header field to the config file + let config_file_path = temp_dir.path().join("changelog.toml"); + let mut file = std::fs::OpenOptions::new() + .append(true) + .write(true) + .open(config_file_path) + .unwrap(); + + writeln!(file, "[header_fields.field]").unwrap(); + writeln!(file, r#"type = ["foo", "bar"]"#).unwrap(); + writeln!(file, "default_value = true").unwrap(); + writeln!(file, "required = true").unwrap(); + file.sync_all().unwrap() + } + + self::common::cargo_changelog_add(temp_dir.path()) + .args([ + "--format=toml", + "--set", + "issue=123", + "--set", + "subject='This is some text'", + "--set", + "field=baz", + "--set", + "type=Bugfix", + ]) + .assert() + .failure(); +} diff --git a/tests/add_command_creates_default_header.rs b/tests/add_command_creates_default_header.rs new file mode 100644 index 0000000..1f741f0 --- /dev/null +++ b/tests/add_command_creates_default_header.rs @@ -0,0 +1,87 @@ +use std::io::Write; + +mod common; + +#[test] +fn new_command_creates_default_header() { + let temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog") + .tempdir() + .unwrap(); + self::common::init_git(temp_dir.path()); + self::common::init_cargo_changelog(temp_dir.path()); + + { + // Write some header field to the config file + let config_file_path = temp_dir.path().join("changelog.toml"); + let mut file = std::fs::OpenOptions::new() + .append(true) + .write(true) + .open(config_file_path) + .unwrap(); + + writeln!(file, "[header_fields.field]").unwrap(); + writeln!(file, r#"type = "bool""#).unwrap(); + writeln!(file, "default_value = true").unwrap(); + writeln!(file, "required = true").unwrap(); + + writeln!(file, "[header_fields.number]").unwrap(); + writeln!(file, r#"type = "int""#).unwrap(); + writeln!(file, "required = true").unwrap(); + file.sync_all().unwrap() + } + + self::common::cargo_changelog_add(temp_dir.path()) + .args([ + "--format=toml", + "--set", + "issue=123", + "--set", + "number=345", + "--set", + "subject='This is some text'", + "--set", + "type=Misc", + ]) + .assert() + .success(); + + let unreleased_dir = temp_dir.path().join(".changelogs").join("unreleased"); + let fragment = std::fs::read_dir(unreleased_dir) + .unwrap() + .into_iter() + .find(|rde| match rde { + Ok(de) => !de.path().ends_with(".gitkeep"), + Err(_) => true, + }) + .unwrap() + .unwrap(); + + let new_fragment_file_contents = std::fs::read_to_string(fragment.path()).unwrap(); + let toml_header = new_fragment_file_contents + .lines() + .skip(1) + .take_while(|line| *line != "+++") + .collect::>() + .join("\n"); + + let toml = toml::from_str::(&toml_header); + assert!( + toml.is_ok(), + "Failed to parse fragment file: {:?}", + toml.unwrap_err() + ); + let toml = toml.unwrap(); + + let field = toml.get("field").unwrap(); + assert!(field.is_bool()); + assert!(std::matches!(field, toml::Value::Boolean(true))); + + let number = toml.get("number").unwrap(); + assert!(number.is_integer()); + assert_eq!(number.as_integer().unwrap(), 345); + + let number = toml.get("issue").unwrap(); + assert!(number.is_integer()); + assert_eq!(number.as_integer().unwrap(), 123); +} diff --git a/tests/add_command_editor.rs b/tests/add_command_editor.rs new file mode 100644 index 0000000..bc7b4af --- /dev/null +++ b/tests/add_command_editor.rs @@ -0,0 +1,112 @@ +use std::io::Write; + +use assert_cmd::Command; + +mod common; + +// In this test implementation we use a trick: +// +// We create a shell script that "edits" a file by creating a file next to it with the same name + +// ".edited". +// +// We set this script as EDITOR and VISUAL and then execute the "add" command. If the test sees the +// "*.edited" file, it knows that the editor was called +// + +const EDITOR_COMMAND_SCRIPT: &str = r#"#!/bin/sh +touch "${1}.edited" +"#; + +#[test] +fn add_command_opens_editor() { + let temp_dir = tempfile::Builder::new() + .prefix("cargo-changelog") + .tempdir() + .unwrap(); + self::common::init_git(temp_dir.path()); + self::common::init_cargo_changelog(temp_dir.path()); + + let (script_temp_dir, editor_script_path) = { + let temp = tempfile::Builder::new() + .prefix("cargo-changelog-add-editor-script-helper") + .tempdir() + .unwrap(); + let script_path = temp.path().join("editor"); + let mut script = std::fs::OpenOptions::new() + .create(true) + .append(false) + .write(true) + .open(&script_path) + .unwrap(); + write!(script, "{EDITOR_COMMAND_SCRIPT}").unwrap(); + script.sync_all().unwrap(); + { + use std::os::unix::fs::PermissionsExt; + let mut p = script.metadata().unwrap().permissions(); + p.set_mode(0o744); + script.set_permissions(p).unwrap(); + } + + assert!( + script_path.exists(), + "Does not exist: {}", + script_path.display() + ); + assert!( + script_path.is_file(), + "Not a file: {}", + script_path.display() + ); + + (temp, script_path) + }; + + Command::cargo_bin("cargo-changelog") + .unwrap() + .envs( + [ + ("EDITOR", editor_script_path.display().to_string()), + ("VISUAL", editor_script_path.display().to_string()), + ] + .into_iter(), + ) + .args([ + "add", + "--interactive=false", + "--format=toml", + "--set", + "issue=123", + "--set", + "subject='This is some text'", + "--set", + "type=Misc", + ]) + .current_dir(&temp_dir) + .assert() + .success(); + + drop(editor_script_path); + drop(script_temp_dir); + + let unreleased_dir = temp_dir.path().join(".changelogs").join("unreleased"); + let files = std::fs::read_dir(unreleased_dir) + .unwrap() + .into_iter() + .filter_map(|direntry| match direntry { + Ok(direntry) => { + if direntry.path().display().to_string().ends_with("edited") { + Some(direntry) + } else { + None + } + } + Err(e) => panic!("Error while iterating over directory: {e:?}"), + }) + .collect::>(); + assert_eq!( + files.len(), + 1, + "Expected 1 file to be found, found: {}: {files:?}", + files.len() + ); +} diff --git a/tests/common.rs b/tests/common.rs index efebe17..b28970b 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -39,9 +39,9 @@ pub fn cargo_changelog_cmd(dir: &std::path::Path) -> assert_cmd::Command { cmd } -pub fn cargo_changelog_new(dir: &std::path::Path) -> assert_cmd::Command { +pub fn cargo_changelog_add(dir: &std::path::Path) -> assert_cmd::Command { let mut cmd = cargo_changelog_cmd(dir); - cmd.arg("new"); + cmd.arg("add"); cmd.arg("--interactive"); cmd.arg("false"); cmd.arg("--edit"); diff --git a/tests/generate_command.rs b/tests/generate_command.rs index e01c58b..2f340ac 100644 --- a/tests/generate_command.rs +++ b/tests/generate_command.rs @@ -43,7 +43,7 @@ fn generate_command_moves_from_unreleased_dir() { self::common::init_cargo(temp_dir.path(), "cargo-changelog-testpkg-generatecommand"); self::common::init_cargo_changelog(temp_dir.path()); - self::common::cargo_changelog_new(temp_dir.path()) + self::common::cargo_changelog_add(temp_dir.path()) .args([ "--format=toml", "--set", diff --git a/tests/new_command.rs b/tests/new_command.rs deleted file mode 100644 index fb32a43..0000000 --- a/tests/new_command.rs +++ /dev/null @@ -1,323 +0,0 @@ -use std::io::Write; - -mod common; - -#[test] -fn new_command_creates_toml_file() { - let temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog") - .tempdir() - .unwrap(); - self::common::init_git(temp_dir.path()); - self::common::init_cargo_changelog(temp_dir.path()); - - self::common::cargo_changelog_new(temp_dir.path()) - .args([ - "--format=toml", - "--set", - "issue=123", - "--set", - "subject=This is some text", - "--set", - "type=Bugfix", - ]) - .assert() - .success(); - - let unreleased_dir = temp_dir.path().join(".changelogs").join("unreleased"); - if !unreleased_dir.exists() { - panic!("Unreleased directory does not exist"); - } - - let files = std::fs::read_dir(&unreleased_dir) - .unwrap() - .into_iter() - .collect::>(); - assert_eq!( - files.len(), - 2, - "Expected 2 entries in unreleased directory, found {}: {:?}", - files.len(), - files - ); - - let new_fragment_file = files - .into_iter() - .find(|rde| match rde { - Ok(de) => !de.path().ends_with(".gitkeep"), - Err(_) => true, - }) - .unwrap() - .unwrap(); - { - let ft = new_fragment_file.file_type().unwrap(); - assert!( - ft.is_file(), - "Expected {} to be a file, is {:?}", - new_fragment_file.path().display(), - ft - ); - } - - let new_fragment_file_contents = std::fs::read_to_string(new_fragment_file.path()).unwrap(); - let toml_header = new_fragment_file_contents - .lines() - .skip(1) - .take_while(|line| *line != "+++") - .collect::>() - .join("\n"); - - let toml = toml::from_str::(&toml_header); - assert!( - toml.is_ok(), - "Failed to parse fragment file: {:?}", - toml.unwrap_err() - ); -} - -#[test] -fn new_command_creates_unreleased_gitkeep() { - let temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog") - .tempdir() - .unwrap(); - self::common::init_git(temp_dir.path()); - self::common::init_cargo_changelog(temp_dir.path()); - - let unreleased_gitkeep_path = temp_dir - .path() - .join(".changelogs") - .join("unreleased") - .join(".gitkeep"); - if !unreleased_gitkeep_path.exists() { - panic!("unreleased gitkeep file does not exist"); - } - if !unreleased_gitkeep_path.is_file() { - panic!("unreleased gitkeep file is not a file"); - } -} - -#[test] -fn new_command_with_text_creates_toml_with_text_from_stdin() { - let temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog") - .tempdir() - .unwrap(); - self::common::init_git(temp_dir.path()); - self::common::init_cargo_changelog(temp_dir.path()); - - let test_text = "This is a test text"; - { - let text_temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog-new-test-text") - .tempdir() - .unwrap(); - let path = text_temp_dir.path().join("text_file.txt"); - let mut file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .append(false) - .open(&path) - .unwrap(); - - write!(file, "{test_text}").unwrap(); - file.sync_all().unwrap(); - drop(file); // make sure we close the handle - - self::common::cargo_changelog_new(temp_dir.path()) - .args([ - "--format=toml", - "--set", - "issue=123", - "--set", - "subject='This is some text'", - "--set", - "type=Bugfix", - "--read=-", // read text from STDIN - ]) - .pipe_stdin(path) - .unwrap() - .assert() - .success(); - } - - let fragment_file = std::fs::read_dir(temp_dir.path().join(".changelogs").join("unreleased")) - .unwrap() - .into_iter() - .find(|rde| match rde { - Ok(de) => !de.path().ends_with(".gitkeep"), - Err(_) => true, - }) - .unwrap() - .unwrap(); - - let new_fragment_file_contents = std::fs::read_to_string(fragment_file.path()).unwrap(); - let contents = new_fragment_file_contents - .lines() - .skip(1) - .skip_while(|line| *line != "+++") - .skip(1) - .collect::>() - .join("\n"); - - assert_eq!(contents, test_text); -} - -#[test] -fn new_command_with_text_creates_toml_with_text_from_file() { - let temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog") - .tempdir() - .unwrap(); - self::common::init_git(temp_dir.path()); - self::common::init_cargo_changelog(temp_dir.path()); - - let test_text = "This is a test text"; - { - let text_temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog-new-test-text") - .tempdir() - .unwrap(); - let path = text_temp_dir.path().join("text_file.txt"); - let mut file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .append(false) - .open(&path) - .unwrap(); - - write!(file, "{test_text}").unwrap(); - file.sync_all().unwrap(); - drop(file); // make sure we close the handle - - self::common::cargo_changelog_new(temp_dir.path()) - .args([ - "--format=toml", - "--set", - "issue=123", - "--set", - "subject='This is some text'", - "--set", - "type=Bugfix", - // read text from PATH - "--read", - &path.display().to_string(), - ]) - .pipe_stdin(path) - .unwrap() - .assert() - .success(); - } - - let fragment_file = std::fs::read_dir(temp_dir.path().join(".changelogs").join("unreleased")) - .unwrap() - .into_iter() - .find(|rde| match rde { - Ok(de) => !de.path().ends_with(".gitkeep"), - Err(_) => true, - }) - .unwrap() - .unwrap(); - - let new_fragment_file_contents = std::fs::read_to_string(fragment_file.path()).unwrap(); - let contents = new_fragment_file_contents - .lines() - .skip(1) - .skip_while(|line| *line != "+++") - .skip(1) - .collect::>() - .join("\n"); - - assert_eq!(contents, test_text); -} - -#[test] -fn new_command_creates_toml_header() { - let temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog") - .tempdir() - .unwrap(); - self::common::init_git(temp_dir.path()); - self::common::init_cargo_changelog(temp_dir.path()); - - self::common::cargo_changelog_new(temp_dir.path()) - .args([ - "--format=toml", - "--set", - "issue=123", - "--set", - "subject='This is some text'", - "--set", - "type=Bugfix", - ]) - .assert() - .success(); - - let unreleased_dir = temp_dir.path().join(".changelogs").join("unreleased"); - - let new_fragment_file = std::fs::read_dir(unreleased_dir) - .unwrap() - .into_iter() - .find(|rde| match rde { - Ok(de) => !de.path().ends_with(".gitkeep"), - Err(_) => true, - }) - .unwrap() - .unwrap(); - - let new_fragment_file_contents = std::fs::read_to_string(new_fragment_file.path()).unwrap(); - let toml_header = new_fragment_file_contents - .lines() - .skip(1) - .take_while(|line| *line != "+++") - .collect::>() - .join("\n"); - - let toml = toml::from_str::(&toml_header); - assert!( - toml.is_ok(), - "Failed to parse fragment file: {:?}", - toml.unwrap_err() - ); -} - -#[test] -fn new_command_cannot_create_nonexistent_oneof() { - let temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog") - .tempdir() - .unwrap(); - self::common::init_git(temp_dir.path()); - self::common::init_cargo_changelog(temp_dir.path()); - - { - // Write some header field to the config file - let config_file_path = temp_dir.path().join("changelog.toml"); - let mut file = std::fs::OpenOptions::new() - .append(true) - .write(true) - .open(config_file_path) - .unwrap(); - - writeln!(file, "[header_fields.field]").unwrap(); - writeln!(file, r#"type = ["foo", "bar"]"#).unwrap(); - writeln!(file, "default_value = true").unwrap(); - writeln!(file, "required = true").unwrap(); - file.sync_all().unwrap() - } - - self::common::cargo_changelog_new(temp_dir.path()) - .args([ - "--format=toml", - "--set", - "issue=123", - "--set", - "subject='This is some text'", - "--set", - "field=baz", - "--set", - "type=Bugfix", - ]) - .assert() - .failure(); -} diff --git a/tests/new_command_creates_default_header.rs b/tests/new_command_creates_default_header.rs deleted file mode 100644 index 577df7c..0000000 --- a/tests/new_command_creates_default_header.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::io::Write; - -mod common; - -#[test] -fn new_command_creates_default_header() { - let temp_dir = tempfile::Builder::new() - .prefix("cargo-changelog") - .tempdir() - .unwrap(); - self::common::init_git(temp_dir.