use std::io::Write;
fn git_init(path: &std::path::Path) -> std::path::PathBuf {
std::process::Command::new("git")
.args(["init", "-b", "main"])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.email", "t@t"])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "t"])
.current_dir(path)
.output()
.unwrap();
let init = path.join("init");
std::fs::write(&init, "").unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(path)
.output()
.unwrap();
init
}
fn git_commit(repo: &std::path::Path) {
std::fs::write(repo.join("f"), "x").unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "x"])
.current_dir(repo)
.output()
.unwrap();
}
fn git_tag(repo: &std::path::Path, tag: &str) {
std::process::Command::new("git")
.args(["-C", repo.to_str().unwrap(), "tag", tag])
.output()
.unwrap();
}
#[test]
fn test_status_empty_repo_no_tags() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
qtcloud_devops_cli::release::status(dir.path());
assert!(!dir.path().join("CHANGELOG.md").exists());
}
#[test]
fn test_status_with_root_tag() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
git_tag(dir.path(), "v1.0.0");
qtcloud_devops_cli::release::status(dir.path());
let output = std::process::Command::new("git")
.args(["-C", dir.path().to_str().unwrap(), "tag"])
.output()
.unwrap();
let tags = String::from_utf8_lossy(&output.stdout);
assert!(tags.contains("v1.0.0"));
}
#[test]
fn test_status_with_scoped_tags() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
git_tag(dir.path(), "cli/v0.1.0");
git_tag(dir.path(), "web/v0.2.0");
qtcloud_devops_cli::release::status(dir.path());
}
#[test]
fn test_status_dirty_workspace() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
git_tag(dir.path(), "v1.0.0");
std::fs::write(dir.path().join("dirty"), "modified").unwrap();
qtcloud_devops_cli::release::status(dir.path());
}
#[test]
fn test_status_with_changelog() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
std::fs::write(dir.path().join("CHANGELOG.md"), "## [1.0.0]\n\ncontent\n").unwrap();
git_commit(dir.path()); git_tag(dir.path(), "v1.0.0");
qtcloud_devops_cli::release::status(dir.path());
}
#[test]
fn test_status_missing_changelog() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
git_tag(dir.path(), "v1.0.0");
qtcloud_devops_cli::release::status(dir.path());
}
#[test]
fn test_status_with_unreleased_commits() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
git_tag(dir.path(), "v1.0.0");
git_commit(dir.path());
git_commit(dir.path());
qtcloud_devops_cli::release::status(dir.path());
}
#[test]
fn test_status_no_remote_no_gh() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
git_tag(dir.path(), "v1.0.0");
qtcloud_devops_cli::release::status(dir.path());
}
#[test]
fn test_release_create_tag_uses_repo_path() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
git_commit(dir.path());
let tag = "v999.999.999-test-repo-path";
assert!(qtcloud_devops_cli::release::create_tag(tag, dir.path()));
let output = std::process::Command::new("git")
.args(["-C", dir.path().to_str().unwrap(), "tag", "-l"])
.output()
.unwrap();
assert!(String::from_utf8_lossy(&output.stdout).contains(tag));
let cwd_tags = std::process::Command::new("git")
.args(["tag", "-l"])
.output()
.unwrap();
assert!(!String::from_utf8_lossy(&cwd_tags.stdout).contains(tag));
}
#[test]
fn test_release_publish_rejects_invalid_version() {
assert!(qtcloud_devops_cli::release::publish(
"bad",
tempfile::tempdir().unwrap().path(),
true,
None
)
.is_err());
}
#[test]
fn test_release_publish_auto_generates_changelog() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
let result = qtcloud_devops_cli::release::publish("v1.0.0", dir.path(), true, None);
assert!(
result.is_ok(),
"publish with auto-generated CHANGELOG 应成功, 得到: {:?}",
result
);
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap_or_default();
assert!(
changelog.contains("## [1.0.0]"),
"CHANGELOG 应包含版本条目, 得到: {}",
changelog
);
}
#[test]
fn test_release_publish_idempotent() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
std::fs::write(
dir.path().join("CHANGELOG.md"),
"## [1.0.0-rc.1]\n\ncontent\n",
)
.unwrap();
assert!(qtcloud_devops_cli::release::publish("v1.0.0-rc.1", dir.path(), true, None).is_ok());
assert!(qtcloud_devops_cli::release::publish("v1.0.0-rc.1", dir.path(), true, None).is_ok());
}
#[test]
fn test_release_publish_without_changelog_entry() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
std::fs::write(dir.path().join("CHANGELOG.md"), "## [1.0.0]\n\ncontent\n").unwrap();
let result = qtcloud_devops_cli::release::publish("v2.0.0", dir.path(), true, None);
assert!(
result.is_ok(),
"LLM 应自动生成缺失的 CHANGELOG 条目, 得到: {:?}",
result
);
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap_or_default();
assert!(
changelog.contains("## [2.0.0]"),
"CHANGELOG 应包含自动生成的 v2.0.0 条目"
);
assert!(changelog.contains("## [1.0.0]"), "原有的 v1.0.0 条目应保留");
}
#[test]
fn test_release_publish_with_v_prefix_changelog() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
std::fs::write(
dir.path().join("CHANGELOG.md"),
"# CHANGELOG\n\n## [v0.1.0]\n\n### Added\n- init\n",
)
.unwrap();
let result = qtcloud_devops_cli::release::publish("v0.1.0", dir.path(), true, None);
assert!(result.is_ok());
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap_or_default();
assert_eq!(changelog.matches("## [").count(), 1, "不应产生重复标题");
}
#[test]
fn test_release_publish_extract_notes_filters_headers() {
let dir = tempfile::tempdir().unwrap();
git_init(dir.path());
std::fs::write(
dir.path().join("CHANGELOG.md"),
"# CHANGELOG\n\n## [1.0.0] - 2026-06-28\n\n\
## [v1.0.0] - 2023-08-31\n\n\
### Added\n- init\n",
)
.unwrap();
let notes =
qtcloud_devops_cli::release::extract_notes("v1.0.0", &dir.path().join("CHANGELOG.md"))
.unwrap_or_default();
assert!(!notes.contains("## ["), "Release notes 应过滤 ## [ 行");
assert!(notes.contains("### Added"));
assert!(notes.contains("- init"));
}