mod common;
use indoc::indoc;
use std::fs;
use tempfile::TempDir;
use verctl::bump::apply;
use verctl::config::Config;
use verctl::driver::Driver;
use verctl::fragment::{Bump, parse_str};
use verctl::prepare;
#[test]
fn patch_and_minor() {
assert_eq!(apply("1.2.3", Bump::Patch, false).expect("p"), "1.2.4");
assert_eq!(apply("1.2.3", Bump::Minor, false).expect("m"), "1.3.0");
assert_eq!(apply("0.0.22", Bump::Patch, false).expect("0p"), "0.0.23");
assert_eq!(apply("0.1.4", Bump::Minor, false).expect("0m"), "0.2.0");
}
#[test]
fn rejects_major_on_0x() {
let error = apply("0.0.22", Bump::Major, false).expect_err("major");
assert!(format!("{error:#}").contains("0.x"), "{error:#}");
}
#[test]
fn major_on_1x() {
assert_eq!(apply("1.4.2", Bump::Major, false).expect("1"), "2.0.0");
}
#[test]
fn release_bump_clears_prerelease_and_build() {
assert_eq!(
apply("1.2.3-rc.1+build5", Bump::Patch, false).expect("p"),
"1.2.4"
);
assert_eq!(
apply("1.2.3-rc.1+build5", Bump::Minor, false).expect("m"),
"1.3.0"
);
assert_eq!(
apply("1.4.2-beta.2+exp", Bump::Major, false).expect("M"),
"2.0.0"
);
}
#[test]
fn none_keeps_prerelease_and_build() {
assert_eq!(
apply("1.2.3-rc.1+build5", Bump::None, false).expect("none"),
"1.2.3-rc.1+build5"
);
}
#[test]
fn cargo_workspace_keeps_comments() {
let raw = indoc! {r#"
# keep me
[workspace.package]
version = "0.0.21" # pin
edition = "2024"
[workspace]
members = ["crates/app"]
"#};
let driver = Driver::cargo().expect("cargo");
let next = driver.write(raw, "0.0.22").expect("write");
assert!(next.contains("# keep me"), "{next}");
assert!(next.contains("# pin"), "{next}");
assert!(next.contains("edition = \"2024\""), "{next}");
assert!(!next.contains("members = [\"other\"]"), "{next}");
assert_eq!(driver.read(&next).expect("read"), "0.0.22");
}
#[test]
fn cargo_package_table() {
let raw = indoc! {r#"
[package]
name = "demo"
version = "1.0.0"
"#};
let driver = Driver::cargo().expect("cargo");
let next = driver.write(raw, "1.0.1").expect("write");
assert_eq!(driver.read(&next).expect("read"), "1.0.1");
assert!(next.contains("name = \"demo\""));
}
#[test]
fn npm_replaces_only_the_version_string() {
let raw = indoc! {r#"
{
"name": "@scope/pkg",
"version": "2.0.0",
"private": true
}
"#};
let next = Driver::npm()
.expect("npm")
.write(raw, "2.1.0")
.expect("write");
assert_eq!(
next,
indoc! {r#"
{
"name": "@scope/pkg",
"version": "2.1.0",
"private": true
}
"#}
);
}
#[test]
fn prepare_fails_closed_on_unknown_package() {
let root = TempDir::new().expect("tmp");
common::write_config(
root.path(),
indoc! {"
packages:
- name: known
path: Cargo.toml
"},
);
fs::write(
root.path().join("Cargo.toml"),
indoc! {r#"
[package]
name = "known"
version = "0.1.0"
"#},
)
.expect("cargo");
let config = Config::load(&root.path().join(verctl::config::FILE)).expect("load");
let fragment = parse_str(
indoc! {"
---
ghost: patch
---
Nope.
"},
"ghost.md",
)
.expect("frag");
let error = prepare::plan(&config, &[fragment], root.path()).expect_err("unknown");
assert!(format!("{error:#}").contains("ghost"), "{error:#}");
}
#[test]
fn prepare_applies_max_bump_across_fragments() {
let root = TempDir::new().expect("tmp");
common::write_config(
root.path(),
indoc! {"
packages:
- name: demo
path: Cargo.toml
"},
);
fs::write(
root.path().join("Cargo.toml"),
indoc! {r#"
[package]
name = "demo"
version = "1.2.3"
"#},
)
.expect("cargo");
let config = Config::load(&root.path().join(verctl::config::FILE)).expect("load");
let patch = parse_str(
indoc! {"
---
demo: patch
---
Fix.
"},
"a.md",
)
.expect("p");
let minor = parse_str(
indoc! {"
---
demo: minor
---
Feat.
"},
"b.md",
)
.expect("m");
let plan = prepare::plan(&config, &[patch, minor], root.path()).expect("plan");
assert_eq!(plan.len(), 1);
assert_eq!(plan[0].from, "1.2.3");
assert_eq!(plan[0].to, "1.3.0");
let follow = prepare::apply_plan(&plan).expect("apply");
assert!(follow.is_empty(), "{follow:?}");
let after = fs::read_to_string(root.path().join("Cargo.toml")).expect("read");
assert!(after.contains("version = \"1.3.0\""));
}
#[test]
fn declared_toml_keys_are_the_same_as_cargo() {
let root = TempDir::new().expect("tmp");
common::write_config(
root.path(),
indoc! {"
drivers:
crate:
format: toml
keys: [package.version]
after: echo locked
packages:
- name: demo
path: Cargo.toml
driver: crate
"},
);
fs::write(
root.path().join("Cargo.toml"),
indoc! {r#"
[package]
name = "demo"
version = "1.0.0"
"#},
)
.expect("cargo");
let config = Config::load(&root.path().join(verctl::config::FILE)).expect("load");
let fragment = parse_str(
indoc! {"
---
demo: patch
---
Patch.
"},
"a.md",
)
.expect("frag");
let plan = prepare::plan(&config, &[fragment], root.path()).expect("plan");
assert_eq!(plan[0].to, "1.0.1");
let follow = prepare::apply_plan(&plan).expect("apply");
assert_eq!(follow, ["echo locked"]);
}
#[test]
fn detects_bun_from_lockfile() {
let root = TempDir::new().expect("tmp");
common::write_config(
root.path(),
indoc! {"
packages:
- name: app
path: package.json
"},
);
fs::write(
root.path().join("package.json"),
indoc! {r#"
{ "name": "app", "version": "1.0.0" }
"#},
)
.expect("pkg");
fs::write(root.path().join("bun.lock"), "{}\n").expect("lock");
let config = Config::load(&root.path().join(verctl::config::FILE)).expect("load");
let fragment = parse_str(
indoc! {"
---
app: patch
---
Patch.
"},
"a.md",
)
.expect("frag");
let plan = prepare::plan(&config, &[fragment], root.path()).expect("plan");
assert_eq!(plan[0].driver.after(), Some("bun install"));
}
#[test]
fn a_cargo_lock_asks_for_the_narrowest_refresh() {
let root = TempDir::new().expect("tmp");
common::write_config(
root.path(),
indoc! {"
packages:
- name: app
path: Cargo.toml
"},
);
fs::write(
root.path().join("Cargo.toml"),
indoc! {r#"
[package]
name = "app"
version = "1.0.0"
"#},
)
.expect("manifest");
fs::write(root.path().join("Cargo.lock"), "version = 4\n").expect("lock");
let config = Config::load(&root.path().join(verctl::config::FILE)).expect("load");
let fragment = parse_str(
indoc! {"
---
app: patch
---
Patch.
"},
"a.md",
)
.expect("frag");
let plan = prepare::plan(&config, &[fragment], root.path()).expect("plan");
assert_eq!(plan[0].driver.after(), Some("cargo update --workspace"));
}
#[test]
fn no_cargo_lock_asks_for_nothing() {
let root = TempDir::new().expect("tmp");
common::write_config(
root.path(),
indoc! {"
packages:
- name: app
path: Cargo.toml
"},
);
fs::write(
root.path().join("Cargo.toml"),
indoc! {r#"
[package]
name = "app"
version = "1.0.0"
"#},
)
.expect("manifest");
let config = Config::load(&root.path().join(verctl::config::FILE)).expect("load");
let fragment = parse_str(
indoc! {"
---
app: patch
---
Patch.
"},
"a.md",
)
.expect("frag");
let plan = prepare::plan(&config, &[fragment], root.path()).expect("plan");
assert_eq!(plan[0].driver.after(), None);
}
#[test]
fn detects_pnpm_from_lockfile() {
let root = TempDir::new().expect("tmp");
common::write_config(
root.path(),
indoc! {"
packages:
- name: app
path: package.json
"},
);
fs::write(
root.path().join("package.json"),
indoc! {r#"
{ "name": "app", "version": "1.0.0" }
"#},
)
.expect("pkg");
fs::write(root.path().join("pnpm-lock.yaml"), "lockfileVersion: '9'\n").expect("lock");
let config = Config::load(&root.path().join(verctl::config::FILE)).expect("load");
let fragment = parse_str(
indoc! {"
---
app: patch
---
Patch.
"},
"a.md",
)
.expect("frag");
let plan = prepare::plan(&config, &[fragment], root.path()).expect("plan");
assert_eq!(plan[0].driver.after(), Some("pnpm install"));
}
#[test]
fn package_after_overrides_detection() {
let root = TempDir::new().expect("tmp");
common::write_config(
root.path(),
indoc! {"
packages:
- name: app
path: package.json
after: mise run install
"},
);
fs::write(
root.path().join("package.json"),
indoc! {r#"
{ "name": "app", "version": "1.0.0" }
"#},
)
.expect("pkg");
fs::write(root.path().join("bun.lock"), "{}\n").expect("lock");
let config = Config::load(&root.path().join(verctl::config::FILE)).expect("load");
let fragment = parse_str(
indoc! {"
---
app: patch
---
Patch.
"},
"a.md",
)
.expect("frag");
let plan = prepare::plan(&config, &[fragment], root.path()).expect("plan");
assert_eq!(plan[0].driver.after(), Some("mise run install"));
}
#[test]
fn json_write_follows_the_full_key_path() {
let raw = indoc! {r#"
{
"version": "2.0.0",
"workspaces": {
"app": {
"version": "2.0.0"
}
}
}
"#};
let driver = Driver::Path {
format: verctl::driver::Format::Json,
keys: vec!["workspaces.app.version".into()],
after: None,
};
let next = driver.write(raw, "2.1.0").expect("write");
assert!(next.contains("\"version\": \"2.1.0\""), "{next}");
assert_eq!(next.matches("\"2.0.0\"").count(), 1, "{next}");
assert_eq!(next.matches("\"2.1.0\"").count(), 1, "{next}");
assert_eq!(driver.read(&next).expect("read"), "2.1.0");
}
#[test]
fn argv_driver_reads_and_writes_without_a_shell() {
let driver = Driver::Command {
read: verctl::driver::CommandSpec::Argv(vec!["tr".into(), "-d".into(), "\n".into()]),
write: verctl::driver::CommandSpec::Argv(vec!["printenv".into(), "VERCTL_VERSION".into()]),
after: None,
};
assert_eq!(driver.read("1.2.3\n").expect("read"), "1.2.3");
assert_eq!(
driver.write("1.2.3\n", "1.2.4").expect("write").trim(),
"1.2.4"
);
}
#[test]
fn argv_write_driver_does_not_deadlock_on_large_stdin() {
let driver = Driver::Command {
read: verctl::driver::CommandSpec::Argv(vec!["tr".into(), "-d".into(), "\n".into()]),
write: verctl::driver::CommandSpec::Argv(vec!["cat".into()]),
after: None,
};
let payload = format!("{}\n", "x".repeat(200_000));
let out = driver.write(&payload, "9.9.9").expect("write");
assert_eq!(out, payload);
}
#[test]
fn a_new_package_takes_a_patch() {
assert_eq!(apply("0.0.0", Bump::Patch, false).expect("p"), "0.0.1");
}
#[test]
fn a_new_package_refuses_minor_and_major() {
for bump in [Bump::Minor, Bump::Major] {
let error = apply("0.0.0", bump, false).expect_err("refused");
let message = format!("{error:#}");
assert!(message.contains("0.0.0 refuses"), "{message}");
assert!(message.contains("first_minor"), "{message}");
}
}
#[test]
fn a_pre_release_of_0_0_0_is_still_new() {
let error = apply("0.0.0-alpha.1", Bump::Minor, false).expect_err("refused");
assert!(format!("{error:#}").contains("first_minor"), "{error:#}");
}
#[test]
fn first_minor_opens_a_new_package_at_0_1_0() {
assert_eq!(apply("0.0.0", Bump::Minor, true).expect("m"), "0.1.0");
let error = apply("0.0.0", Bump::Major, true).expect_err("major");
assert!(format!("{error:#}").contains("0.x"), "{error:#}");
}
#[test]
fn none_leaves_a_new_package_alone() {
assert_eq!(apply("0.0.0", Bump::None, false).expect("none"), "0.0.0");
}
#[test]
fn the_first_release_rule_ends_at_0_0_1() {
assert_eq!(apply("0.0.1", Bump::Minor, false).expect("m"), "0.1.0");
}
#[test]
fn prepare_reads_first_minor_from_the_package() {
let root = TempDir::new().expect("tmp");
fs::write(
root.path().join("Cargo.toml"),
indoc! {r#"
[package]
name = "demo"
version = "0.0.0"
"#},
)
.expect("cargo");
let minor = parse_str(
indoc! {"
---
demo: minor
---
First.
"},
"first.md",
)
.expect("m");
let path = common::write_config(
root.path(),
indoc! {"
packages:
- name: demo
path: Cargo.toml
"},
);
let config = Config::load(&path).expect("load");
let error =
prepare::plan(&config, std::slice::from_ref(&minor), root.path()).expect_err("refused");
assert!(format!("{error:#}").contains("package demo"), "{error:#}");
let path = common::write_config(
root.path(),
indoc! {"
packages:
- name: demo
path: Cargo.toml
first_minor: true
"},
);
let config = Config::load(&path).expect("load");
let plan = prepare::plan(&config, &[minor], root.path()).expect("plan");
assert_eq!(plan[0].to, "0.1.0");
}