Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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);
}