use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
struct Repo {
dir: TempDir,
}
impl Repo {
fn init() -> Self {
let dir = tempfile::tempdir().unwrap();
run(dir.path(), &["init", "-q"]);
run(dir.path(), &["config", "user.email", "test@example.com"]);
run(dir.path(), &["config", "user.name", "test"]);
Self { dir }
}
fn path(&self) -> &Path {
self.dir.path()
}
fn commit(&self, message: &str) {
run(
self.path(),
&["commit", "-q", "--allow-empty", "-m", message],
);
}
fn commit_full(&self, message: &str) {
run(
self.path(),
&["commit", "-q", "--allow-empty", "-m", message],
);
}
fn next_version(&self, last_version: &str, range: &str) -> String {
let script = script_path();
let output = Command::new("bash")
.arg(&script)
.arg(last_version)
.arg(range)
.current_dir(self.path())
.output()
.unwrap_or_else(|e| panic!("failed to run {}: {e}", script.display()));
assert!(
output.status.success(),
"next-version.sh exited non-zero: status={:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap().trim().to_string()
}
}
fn run(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(dir)
.status()
.unwrap_or_else(|e| panic!("failed to run git {args:?}: {e}"));
assert!(status.success(), "git {args:?} failed with {status:?}");
}
fn script_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("scripts/next-version.sh")
}
#[test]
fn docs_only_yields_no_release() {
let repo = Repo::init();
repo.commit("chore: seed");
repo.commit("docs: update readme");
assert_eq!(repo.next_version("1.5.3", "HEAD~1..HEAD"), "");
}
#[test]
fn fix_yields_patch_bump() {
let repo = Repo::init();
repo.commit("chore: seed");
repo.commit("fix(core): a bug");
assert_eq!(repo.next_version("1.5.3", "HEAD~1..HEAD"), "1.5.4");
}
#[test]
fn feat_yields_minor_bump() {
let repo = Repo::init();
repo.commit("chore: seed");
repo.commit("feat(cli): a thing");
assert_eq!(repo.next_version("1.5.3", "HEAD~1..HEAD"), "1.6.0");
}
#[test]
fn bang_subject_yields_major_bump() {
let repo = Repo::init();
repo.commit("chore: seed");
repo.commit("feat(cli)!: breaking");
assert_eq!(repo.next_version("1.5.3", "HEAD~1..HEAD"), "2.0.0");
}
#[test]
fn breaking_change_footer_yields_major_bump() {
let repo = Repo::init();
repo.commit("chore: seed");
repo.commit_full("fix(core): a bug\n\nBREAKING CHANGE: removes the old flag");
assert_eq!(repo.next_version("1.5.3", "HEAD~1..HEAD"), "2.0.0");
}
#[test]
fn hyphenated_breaking_change_footer_yields_major_bump() {
let repo = Repo::init();
repo.commit("chore: seed");
repo.commit_full("fix(core): a bug\n\nBREAKING-CHANGE: removes the old flag");
assert_eq!(repo.next_version("1.5.3", "HEAD~1..HEAD"), "2.0.0");
}
#[test]
fn mixed_range_takes_highest_bump() {
let repo = Repo::init();
repo.commit("chore: seed");
repo.commit("docs: readme");
repo.commit("fix(core): a bug");
repo.commit("feat(cli): a thing");
assert_eq!(repo.next_version("1.5.3", "HEAD~3..HEAD"), "1.6.0");
}
#[test]
fn release_commit_alone_yields_no_release() {
let repo = Repo::init();
repo.commit("chore: seed");
repo.commit("chore(release): v1.5.3");
assert_eq!(repo.next_version("1.5.3", "HEAD~1..HEAD"), "");
}