use assert_cmd::Command;
use chrono::Local;
use predicates::prelude::*;
use std::fs;
use std::path::Path;
use std::process::Command as ProcessCommand;
use tempfile::TempDir;
#[test]
fn plan_release_derives_the_next_remote_tag_without_mutating_git() {
let fixture = GitFixture::new();
let today = Local::now().date_naive().format("%Y.%m.%d").to_string();
let previous = format!("{today}.9");
fixture.tag_previous_commit(&previous);
let head = fixture.add_release_commit();
let refs_before = fixture.remote_refs();
Command::cargo_bin("release-tool")
.unwrap()
.current_dir(&fixture.repository)
.args(["plan", "--release"])
.assert()
.success()
.stdout(predicate::str::contains(format!("tag: {today}.10")))
.stdout(predicate::str::contains(format!("commit: {head}")))
.stdout(predicate::str::contains("target: app"));
assert_eq!(fixture.remote_refs(), refs_before);
assert_eq!(fixture.git(&["status", "--porcelain"]), "");
assert_eq!(fixture.git(&["tag", "--points-at", "HEAD"]), "");
}
#[test]
fn plan_without_release_requires_an_existing_annotated_remote_tag() {
let fixture = GitFixture::new();
fixture.add_release_commit();
Command::cargo_bin("release-tool")
.unwrap()
.current_dir(&fixture.repository)
.arg("plan")
.assert()
.failure()
.stderr(predicate::str::contains(
"current commit has no annotated remote calendar release tag; use `--release`",
));
}
#[test]
fn plan_supports_named_targets_and_all_without_running_builds() {
let fixture = GitFixture::new();
fixture.add_release_commit();
Command::cargo_bin("release-tool")
.unwrap()
.current_dir(&fixture.repository)
.args(["plan", "--release", "--target", "rl"])
.assert()
.success()
.stdout(predicate::str::contains("target: rl"))
.stdout(predicate::str::contains("target: app").not());
Command::cargo_bin("release-tool")
.unwrap()
.current_dir(&fixture.repository)
.args(["plan", "--release", "--all"])
.assert()
.success()
.stdout(predicate::str::contains("target: app"))
.stdout(predicate::str::contains("target: rl"));
}
struct GitFixture {
_root: TempDir,
repository: std::path::PathBuf,
remote: std::path::PathBuf,
}
impl GitFixture {
fn new() -> Self {
let root = tempfile::tempdir().unwrap();
let repository = root.path().join("work");
let remote = root.path().join("remote.git");
run(
root.path(),
&["git", "init", "--bare", remote.to_str().unwrap()],
);
run(
root.path(),
&[
"git",
"init",
"--initial-branch=main",
repository.to_str().unwrap(),
],
);
run(&repository, &["git", "config", "user.name", "Release Test"]);
run(
&repository,
&["git", "config", "user.email", "release@example.com"],
);
fs::write(repository.join("seed"), "seed\n").unwrap();
fs::write(
repository.join("release.toml"),
r#"required_version = "0.1.0"
[repository]
github = "computed-parameter/example"
branch = "main"
[publishers.release]
kind = "github_release"
title = "Example {version}"
prerelease = true
[[targets]]
name = "app"
kind = "docker_archive"
publisher = "release"
default = true
platform = "linux/amd64"
image = "example:{version}"
asset = "example-{version}-linux-amd64.tar.xz"
build = ["just", "_build-release-image", "{image}"]
local_check = ["just", "_check-release-image", "{image}"]
[[targets]]
name = "rl"
kind = "docker_archive"
publisher = "release"
platform = "linux/amd64"
image = "example:{version}-rl"
asset = "example-{version}-rl-linux-amd64.tar.xz"
build = ["just", "_build-release-image", "{image}"]
local_check = ["just", "_check-release-image", "{image}"]
"#,
)
.unwrap();
run(&repository, &["git", "add", "--", "seed", "release.toml"]);
run(&repository, &["git", "commit", "-m", "Seed"]);
run(
&repository,
&["git", "remote", "add", "origin", remote.to_str().unwrap()],
);
run(&repository, &["git", "push", "-u", "origin", "main"]);
Self {
_root: root,
repository,
remote,
}
}
fn tag_previous_commit(&self, tag: &str) {
run(&self.repository, &["git", "tag", "-a", tag, "-m", tag]);
run(&self.repository, &["git", "push", "origin", tag]);
}
fn add_release_commit(&self) -> String {
fs::write(self.repository.join("release-change"), "ready\n").unwrap();
run(&self.repository, &["git", "add", "--", "release-change"]);
run(&self.repository, &["git", "commit", "-m", "Release change"]);
run(&self.repository, &["git", "push", "origin", "main"]);
self.git(&["rev-parse", "HEAD"])
}
fn git(&self, arguments: &[&str]) -> String {
let output = ProcessCommand::new("git")
.args(arguments)
.current_dir(&self.repository)
.output()
.unwrap();
assert!(output.status.success(), "git {:?} failed", arguments);
String::from_utf8(output.stdout).unwrap().trim().to_owned()
}
fn remote_refs(&self) -> String {
run_output(
self._root.path(),
&[
"git",
"--git-dir",
self.remote.to_str().unwrap(),
"show-ref",
],
)
}
}
fn run(directory: &Path, command: &[&str]) {
let output = ProcessCommand::new(command[0])
.args(&command[1..])
.current_dir(directory)
.output()
.unwrap();
assert!(
output.status.success(),
"command {:?} failed:\n{}",
command,
String::from_utf8_lossy(&output.stderr)
);
}
fn run_output(directory: &Path, command: &[&str]) -> String {
let output = ProcessCommand::new(command[0])
.args(&command[1..])
.current_dir(directory)
.output()
.unwrap();
assert!(output.status.success(), "command {:?} failed", command);
String::from_utf8(output.stdout).unwrap()
}