xtask/tests/publish_run.rs
//! `cargo xtask publish` against a throwaway repository: the refusals, and a public
//! branch written from HEAD minus the exclusion list that is then refused for not
//! building (`xtask/src/publish.rs`).
use std::path::PathBuf;
use std::process::Command;
use xtask::publish::{head_entries, run, Report};
/// `<tmp>/<unique>/repo`: the throwaway check worktree lands beside `repo`.
struct Repo {
base: PathBuf,
root: PathBuf,
}
impl Repo {
fn new(name: &str) -> Repo {
let base =
std::env::temp_dir().join(format!("gunnar-publish-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let root = base.join("repo");
std::fs::create_dir_all(&root).unwrap();
let r = Repo { base, root };
r.git(&["init", "-q", "-b", "master"]);
// `run` commits the public tree under the repository's own identity
r.git(&["config", "user.name", "t"]);
r.git(&["config", "user.email", "t@mock.invalid"]);
r
}
fn git(&self, args: &[&str]) -> String {
let out = Command::new("git")
.current_dir(&self.root)
.args(["-c", "user.name=t", "-c", "user.email=t@mock.invalid"])
.args(args)
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn write(&self, rel: &str, text: &str) {
let p = self.root.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, text).unwrap();
}
fn commit(&self, msg: &str) {
self.git(&["add", "-A"]);
self.git(&["commit", "-q", "-m", msg]);
}
}
impl Drop for Repo {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.base);
}
}
fn product(r: &Repo) {
r.write("Cargo.toml", "[workspace]\nmembers = [\"crates/gunnar-core\"]\nresolver = \"2\"\n\n[workspace.package]\nversion = \"9.8.7\"\n");
r.write(
"crates/gunnar-core/Cargo.toml",
"[package]\nname = \"gunnar-core\"\nversion.workspace = true\nedition = \"2021\"\n",
);
r.write("README.md", "# gunnar\n");
r.write("crates/gunnar-core/src/lib.rs", "pub fn x() {}\n");
r.write(".nornir/design.md", "the record\n");
r.write("bench/run.sh", "#!/bin/sh\n");
r.write("locked_on_target.md", "a note\n");
}
/// HEAD's tree is read path by path, with mode and oid.
#[test]
fn head_entries_lists_every_tracked_path() {
let r = Repo::new("entries");
product(&r);
r.commit("one");
let mut paths: Vec<String> = head_entries(&r.root)
.unwrap()
.into_iter()
.map(|e| e.path)
.collect();
paths.sort();
assert_eq!(
paths,
[
".nornir/design.md",
"Cargo.toml",
"README.md",
"bench/run.sh",
"crates/gunnar-core/Cargo.toml",
"crates/gunnar-core/src/lib.rs",
"locked_on_target.md"
]
);
let e = head_entries(&r.root)
.unwrap()
.into_iter()
.find(|e| e.path == "README.md")
.unwrap();
assert_eq!(e.mode, "100644");
assert_eq!(e.oid.len(), 40, "{e:?}");
let nowhere = r.base.join("not-a-repo");
std::fs::create_dir_all(&nowhere).unwrap();
assert!(head_entries(&nowhere).is_err());
}
/// A dirty tree and a branch other than master are refused before anything
/// is written; so is a tree whose front page has no source.
#[test]
fn nothing_is_published_from_a_dirty_tree_a_side_branch_or_without_a_front_page() {
let r = Repo::new("refuse");
product(&r);
r.commit("one");
r.write("README.md", "# edited\n");
let e = run(&r.root, false).unwrap_err();
assert!(
e.contains("working tree is dirty") && e.contains("README.md"),
"{e}"
);
r.git(&["checkout", "-q", "--", "README.md"]);
r.git(&["checkout", "-q", "-b", "work"]);
let e = run(&r.root, false).unwrap_err();
assert!(e.contains("HEAD is on `work`"), "{e}");
r.git(&["checkout", "-q", "master"]);
let e = run(&r.root, false).unwrap_err();
assert!(e.contains("no `.nornir/README.md`"), "{e}");
r.write(".nornir/README.md", "# the front page\n");
r.git(&["rm", "-q", "README.md"]);
r.commit("front page source, no README");
let e = run(&r.root, false).unwrap_err();
assert!(e.contains("no `README.md`"), "{e}");
let refs = r.git(&["for-each-ref", "--format=%(refname)", "refs/heads/"]);
assert!(
!refs.contains("refs/heads/public"),
"a refused run wrote a branch: {refs}"
);
}
/// The public branch is HEAD minus the exclusion list, committed under the
/// workspace version; and a public tree that does not build is refused by the
/// cargo step that failed, its throwaway worktree removed.
#[test]
fn the_public_branch_drops_the_record_and_a_tree_that_does_not_build_is_refused() {
let r = Repo::new("branch");
product(&r);
r.write(".nornir/README.md", "# the front page\n");
r.commit("one");
let e = run(&r.root, false).unwrap_err();
assert!(
e.contains("the public tree fails `cargo check -p gunnar-cli -p gunnar-server`"),
"{e}"
);
assert!(
!r.base.join("gunnar-public-check").exists(),
"the check worktree was left behind"
);
let listing = r.git(&["ls-tree", "-r", "--name-only", "public"]);
let mut public: Vec<&str> = listing.lines().collect();
public.sort();
assert_eq!(
public,
[
".nornir/README.md",
"Cargo.toml",
"README.md",
"crates/gunnar-core/Cargo.toml",
"crates/gunnar-core/src/lib.rs"
]
);
let msg = r.git(&["log", "-1", "--format=%s", "public"]);
assert!(msg.starts_with("gunnar 9.8.7 — public release "), "{msg}");
assert_eq!(r.git(&["rev-parse", "public^{tree}"]).trim().len(), 40);
assert!(
r.git(&["status", "--porcelain"]).trim().is_empty(),
"the run left the tree dirty"
);
}
#[test]
fn the_counts_are_the_sum_of_the_reasons() {
let rep = Report {
excluded_by_reason: vec![("a", 2), ("b", 5)],
..Report::default()
};
assert_eq!(rep.excluded_total(), 7);
assert_eq!(Report::default().excluded_total(), 0);
}