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).expect("p"), "1.2.4");
assert_eq!(apply("1.2.3", Bump::Minor).expect("m"), "1.3.0");
assert_eq!(apply("0.0.22", Bump::Patch).expect("0p"), "0.0.23");
assert_eq!(apply("0.1.4", Bump::Minor).expect("0m"), "0.2.0");
}
#[test]
fn rejects_major_on_0x() {
let error = apply("0.0.22", Bump::Major).expect_err("major");
assert!(format!("{error:#}").contains("0.x"), "{error:#}");
}
#[test]
fn major_on_1x() {
assert_eq!(apply("1.4.2", Bump::Major).expect("1"), "2.0.0");
}
#[test]
fn release_bump_clears_prerelease_and_build() {
assert_eq!(apply("1.2.3-rc.1+build5", Bump::Patch).expect("p"), "1.2.4");
assert_eq!(apply("1.2.3-rc.1+build5", Bump::Minor).expect("m"), "1.3.0");
assert_eq!(apply("1.4.2-beta.2+exp", Bump::Major).expect("M"), "2.0.0");
}
#[test]
fn none_keeps_prerelease_and_build() {
assert_eq!(
apply("1.2.3-rc.1+build5", Bump::None).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 stream = indoc! {"
import sys
while True:
chunk = sys.stdin.buffer.read(1024)
if not chunk:
break
sys.stdout.buffer.write(chunk)
sys.stdout.buffer.flush()
"};
let driver = Driver::Command {
read: verctl::driver::CommandSpec::Argv(vec!["tr".into(), "-d".into(), "\n".into()]),
write: verctl::driver::CommandSpec::Argv(vec![
"python3".into(),
"-c".into(),
stream.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);
}