use crate::{Config, Language};
use anyhow::Result;
use std::path::Path;
#[derive(Debug)]
pub struct PublishOutput {
pub success: bool,
pub stdout: String,
pub stderr: String,
}
#[must_use]
pub fn resolve_publish_command(
relative_path: &Path,
language: Language,
default_command: &str,
config: &Config,
) -> String {
if let Some(cmd) = config.publish.get(relative_path.to_string_lossy().as_ref()) {
return cmd.clone();
}
let lang_key = language.publish_key();
if let Some(cmd) = config.publish.get(lang_key) {
return cmd.clone();
}
default_command.to_string()
}
#[must_use]
pub fn resolve_dry_run_publish_command(
relative_path: &Path,
language: Language,
default_dry_run_command: Option<&str>,
config: &Config,
) -> Option<String> {
if let Some(cmd) = config
.publish_dry_run
.get(relative_path.to_string_lossy().as_ref())
{
return Some(cmd.clone());
}
if let Some(cmd) = config.publish_dry_run.get(language.publish_key()) {
return Some(cmd.clone());
}
default_dry_run_command.map(str::to_string)
}
#[cfg(target_os = "windows")]
fn build_shell_command(command: &str) -> tokio::process::Command {
let mut c = tokio::process::Command::new("cmd");
c.arg("/C").arg(command);
c
}
#[cfg(not(target_os = "windows"))]
fn build_shell_command(command: &str) -> tokio::process::Command {
let mut c = tokio::process::Command::new("sh");
c.arg("-c").arg(command);
c
}
pub async fn run_publish_command(command: &str, working_dir: &Path) -> Result<PublishOutput> {
let mut cmd = build_shell_command(command);
cmd.current_dir(working_dir);
let output = cmd.output().await?;
Ok(PublishOutput {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}
pub async fn run_publish_command_argv(
program: &str,
args: &[&str],
working_dir: &Path,
kill_on_drop: bool,
) -> Result<PublishOutput> {
let mut cmd = tokio::process::Command::new(program);
cmd.args(args).current_dir(working_dir);
cmd.kill_on_drop(kill_on_drop);
let output = cmd.output().await?;
Ok(PublishOutput {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_resolve_publish_command_by_path() {
let mut publish = HashMap::new();
publish.insert(
"packages/core/package.json".to_string(),
"custom publish".to_string(),
);
let config = Config {
publish,
..Default::default()
};
let result = resolve_publish_command(
Path::new("packages/core/package.json"),
Language::Node,
"npm publish",
&config,
);
assert_eq!(result, "custom publish");
}
#[test]
fn test_resolve_publish_command_by_language() {
let mut publish = HashMap::new();
publish.insert(
"node".to_string(),
"npm publish --access public".to_string(),
);
let config = Config {
publish,
..Default::default()
};
let result = resolve_publish_command(
Path::new("package.json"),
Language::Node,
"npm publish",
&config,
);
assert_eq!(result, "npm publish --access public");
}
#[test]
fn test_resolve_publish_command_default_fallback() {
let config = Config::default();
let result = resolve_publish_command(
Path::new("package.json"),
Language::Node,
"npm publish",
&config,
);
assert_eq!(result, "npm publish");
}
#[test]
fn test_resolve_dry_run_publish_command_by_path() {
let mut publish_dry_run = HashMap::new();
publish_dry_run.insert(
"packages/core/package.json".to_string(),
"custom dry".to_string(),
);
let config = Config {
publish_dry_run,
..Default::default()
};
let result = resolve_dry_run_publish_command(
Path::new("packages/core/package.json"),
Language::Node,
Some("npm publish --dry-run"),
&config,
);
assert_eq!(result.as_deref(), Some("custom dry"));
}
#[test]
fn test_resolve_dry_run_publish_command_by_language() {
let mut publish_dry_run = HashMap::new();
publish_dry_run.insert("node".to_string(), "npm publish --dry-run -tag".to_string());
let config = Config {
publish_dry_run,
..Default::default()
};
let result = resolve_dry_run_publish_command(
Path::new("package.json"),
Language::Node,
Some("npm publish --dry-run"),
&config,
);
assert_eq!(result.as_deref(), Some("npm publish --dry-run -tag"));
}
#[test]
fn test_resolve_dry_run_publish_command_falls_back_to_language_default() {
let config = Config::default();
let result = resolve_dry_run_publish_command(
Path::new("package.json"),
Language::Node,
Some("npm publish --dry-run"),
&config,
);
assert_eq!(result.as_deref(), Some("npm publish --dry-run"));
}
#[test]
fn test_resolve_dry_run_publish_command_unsupported_returns_none() {
let config = Config::default();
let result = resolve_dry_run_publish_command(
Path::new("project.csproj"),
Language::CSharp,
None,
&config,
);
assert!(result.is_none());
}
#[test]
fn test_resolve_dry_run_publish_command_unsupported_with_path_override() {
let mut publish_dry_run = HashMap::new();
publish_dry_run.insert(
"project.csproj".to_string(),
"dotnet pack -c Release".to_string(),
);
let config = Config {
publish_dry_run,
..Default::default()
};
let result = resolve_dry_run_publish_command(
Path::new("project.csproj"),
Language::CSharp,
None,
&config,
);
assert_eq!(result.as_deref(), Some("dotnet pack -c Release"));
}
#[test]
fn test_resolve_dry_run_publish_command_unsupported_with_language_override() {
let mut publish_dry_run = HashMap::new();
publish_dry_run.insert("csharp".to_string(), "dotnet pack -c Release".to_string());
let config = Config {
publish_dry_run,
..Default::default()
};
let result = resolve_dry_run_publish_command(
Path::new("project.csproj"),
Language::CSharp,
None,
&config,
);
assert_eq!(result.as_deref(), Some("dotnet pack -c Release"));
}
#[tokio::test]
async fn test_run_publish_command_success() {
let temp_dir = std::env::temp_dir();
let command = if cfg!(target_os = "windows") {
"cmd /c echo publish"
} else {
"echo publish"
};
let output = run_publish_command(command, &temp_dir).await.unwrap();
assert!(output.success);
assert!(output.stdout.contains("publish"));
}
#[tokio::test]
async fn test_run_publish_command_failure() {
let temp_dir = std::env::temp_dir();
let command = if cfg!(target_os = "windows") {
"cmd /c exit 1"
} else {
"exit 1"
};
let output = run_publish_command(command, &temp_dir).await.unwrap();
assert!(!output.success);
}
#[tokio::test]
async fn test_run_publish_command_argv_success() {
let temp_dir = std::env::temp_dir();
let (program, args): (&str, Vec<&str>) = if cfg!(target_os = "windows") {
("cmd", vec!["/C", "echo", "argv-ok"])
} else {
("echo", vec!["argv-ok"])
};
let output = run_publish_command_argv(program, &args, &temp_dir, true)
.await
.unwrap();
assert!(output.success);
assert!(output.stdout.contains("argv-ok"));
}
#[tokio::test]
async fn test_run_publish_command_argv_failure() {
let temp_dir = std::env::temp_dir();
let (program, args): (&str, Vec<&str>) = if cfg!(target_os = "windows") {
("cmd", vec!["/C", "exit", "1"])
} else {
("sh", vec!["-c", "exit 1"])
};
let output = run_publish_command_argv(program, &args, &temp_dir, true)
.await
.unwrap();
assert!(!output.success);
}
#[tokio::test]
async fn test_run_publish_command_argv_spawn_error() {
let temp_dir = std::env::temp_dir();
let result = run_publish_command_argv(
"this-binary-does-not-exist-changepacks-test",
&[],
&temp_dir,
true,
)
.await;
assert!(result.is_err());
}
#[test]
fn test_build_shell_command() {
let cmd = build_shell_command("echo hello");
let program = cmd.as_std().get_program().to_string_lossy().to_string();
#[cfg(target_os = "windows")]
assert_eq!(program, "cmd");
#[cfg(not(target_os = "windows"))]
assert_eq!(program, "sh");
}
}