crates/cli/tests/m3b_m3c.rs 7.3 KiB 176 lines Raw
1 #![forbid(unsafe_code)]2 //! M3b+M3c e2e: prs, labels, milestones against temp repos via the binary.3 use std::path::{Path, PathBuf};4 use std::process::Command;5 use std::sync::atomic::{AtomicU64, Ordering};6 7 static CTR: AtomicU64 = AtomicU64::new(0);8 9 fn bin() -> String {10     env!("CARGO_BIN_EXE_f0rge").to_string()11 }12 13 fn tmp_repo(tag: &str) -> PathBuf {14     let n = CTR.fetch_add(1, Ordering::Relaxed);15     let d = std::env::temp_dir().join(format!(16         "f0rge-m3-{}-{}-{n}",17         std::process::id(),18         tag19     ));20     let _ = std::fs::remove_dir_all(&d);21     std::fs::create_dir_all(&d).unwrap();22     d23 }24 25 fn git(repo: &Path, args: &[&str]) {26     let o = Command::new("git").args(args).current_dir(repo).output().unwrap();27     assert!(o.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&o.stderr));28 }29 30 fn f0rge(repo: &Path, args: &[&str]) -> (i32, String, String) {31     let o = Command::new(bin()).args(args).current_dir(repo).output().unwrap();32     (33         o.status.code().unwrap_or(-1),34         String::from_utf8_lossy(&o.stdout).into_owned(),35         String::from_utf8_lossy(&o.stderr).into_owned(),36     )37 }38 39 fn setup_repo() -> PathBuf {40     let repo = tmp_repo("e2e");41     git(&repo, &["init"]);42     git(&repo, &["config", "user.email", "[email protected]"]);43     git(&repo, &["config", "user.name", "t"]);44     git(&repo, &["config", "commit.gpgsign", "false"]);45     std::fs::write(repo.join("a.txt"), "line1\nline2\n").unwrap();46     git(&repo, &["add", "-A"]);47     git(&repo, &["commit", "-m", "init"]);48     git(&repo, &["checkout", "-b", "main"]);49     let (c, _, e) = f0rge(&repo, &["init"]);50     assert_eq!(c, 0, "init: {e}");51     repo52 }53 54 fn last_json_line(out: &str) -> serde_json::Value {55     let line = out.lines().last().unwrap_or("").trim();56     serde_json::from_str(line).expect("json line")57 }58 59 #[test]60 fn m3b_prs_happy_and_conflict() {61     let repo = setup_repo();62     // feat branch with a change.63     git(&repo, &["checkout", "-b", "feat"]);64     std::fs::write(repo.join("a.txt"), "line1-changed\nline2\n").unwrap();65     git(&repo, &["add", "-A"]);66     git(&repo, &["commit", "-m", "feat"]);67     git(&repo, &["checkout", "main"]);68 69     // create.70     let issue = "11111111-1111-4111-8111-111111111111";71     let (c, o, e) = f0rge(72         &repo,73         &["pr", "create", "--base", "main", "--head", "feat", "--title", "T1", "--body", "B1", "--issue", issue, "--json"],74     );75     assert_eq!(c, 0, "create: {e}");76     let v = last_json_line(&o);77     assert_eq!(v["title"], "T1");78     assert_eq!(v["state"], "open");79     assert_eq!(v["base"], "main");80     assert_eq!(v["head"], "feat");81     assert_eq!(v["number"], 1);82     let id = v["databaseId"].as_str().unwrap().to_string();83     assert!(repo.join(".forge").join("prs").join(&id).join("patch.diff").exists());84     assert!(repo.join(".forge").join("prs").join(&id).join("pr.md").exists());85 86     // diff prints the patch.87     let (c, o, _) = f0rge(&repo, &["pr", "diff", &id]);88     assert_eq!(c, 0);89     assert!(o.contains("line1-changed"), "diff body");90 91     // list filtering.92     let (c, o, _) = f0rge(&repo, &["pr", "list", "--json", "--state", "open"]);93     assert_eq!(c, 0);94     assert!(o.contains(&id));95     let (c, o, _) = f0rge(&repo, &["pr", "list", "--json", "--state", "closed"]);96     assert_eq!(c, 0);97     assert_eq!(o.trim(), "[]");98 99     // review + comment append files.100     let (c, _, e) = f0rge(&repo, &["pr", "review", &id, "--body", "lgtm", "--author", "z", "--state", "approved"]);101     assert_eq!(c, 0, "review: {e}");102     let (c, _, e) = f0rge(&repo, &["pr", "comment", &id, "--body", "nit"]);103     assert_eq!(c, 0, "comment: {e}");104     let rv: Vec<_> = std::fs::read_dir(repo.join(".forge").join("prs").join(&id).join("reviews")).unwrap().collect();105     assert_eq!(rv.len(), 2);106 107     // merge happy path.108     let (c, _, e) = f0rge(&repo, &["pr", "merge", &id]);109     assert_eq!(c, 0, "merge: {e}");110     let prmd = std::fs::read_to_string(repo.join(".forge").join("prs").join(&id).join("pr.md")).unwrap();111     assert!(prmd.contains("state: merged"));112     let merges: Vec<_> = std::fs::read_dir(repo.join(".forge").join("merges")).unwrap().collect();113     assert_eq!(merges.len(), 1);114 115     // conflict path: new branch off main, conflicting main advance after create.116     git(&repo, &["checkout", "-b", "feat2"]);117     std::fs::write(repo.join("a.txt"), "line1-feat2\nline2\n").unwrap();118     git(&repo, &["add", "-A"]);119     git(&repo, &["commit", "-m", "feat2"]);120     git(&repo, &["checkout", "main"]);121     let (c, o, _) = f0rge(&repo, &["pr", "create", "--base", "main", "--head", "feat2", "--title", "T2", "--json"]);122     assert_eq!(c, 0);123     let id2 = last_json_line(&o)["databaseId"].as_str().unwrap().to_string();124     std::fs::write(repo.join("a.txt"), "line1-main\nline2\n").unwrap();125     git(&repo, &["add", "-A"]);126     git(&repo, &["commit", "-m", "main-advance"]);127     let (c, _, e) = f0rge(&repo, &["pr", "merge", &id2]);128     assert_eq!(c, 1, "conflict must exit 1");129     assert_eq!(e.lines().count(), 1, "one-line error, got: {e}");130     assert!(e.contains("a.txt"), "conflict list names file: {e}");131     let prmd2 = std::fs::read_to_string(repo.join(".forge").join("prs").join(&id2).join("pr.md")).unwrap();132     assert!(prmd2.contains("state: open"), "conflict leaves PR open");133 134     // close path.135     let (c, _, e) = f0rge(&repo, &["pr", "close", &id2]);136     assert_eq!(c, 0, "close: {e}");137     let prmd2 = std::fs::read_to_string(repo.join(".forge").join("prs").join(&id2).join("pr.md")).unwrap();138     assert!(prmd2.contains("state: closed"));139     let (c, o, _) = f0rge(&repo, &["pr", "list", "--json", "--state", "closed"]);140     assert_eq!(c, 0);141     assert!(o.contains(&id2));142     let _ = std::fs::remove_dir_all(&repo);143 }144 145 #[test]146 fn m3c_labels_milestones() {147     let repo = setup_repo();148     // labels.149     let (c, _, e) = f0rge(&repo, &["label", "create", "--name", "bug", "--color", "ff0000", "--description", "Bugs", "--json"]);150     assert_eq!(c, 0, "label create: {e}");151     let (c, o, _) = f0rge(&repo, &["label", "list", "--json"]);152     assert_eq!(c, 0);153     assert!(o.contains("\"bug\""));154     let (c, _, _) = f0rge(&repo, &["label", "create", "--name", "bug"]);155     assert_eq!(c, 2, "duplicate label exits 2");156     let (c, o, e) = f0rge(&repo, &["label", "edit", "--name", "bug", "--color", "00ff00", "--json"]);157     assert_eq!(c, 0, "label edit: {e}");158     assert!(last_json_line(&o)["color"] == "00ff00");159     let (c, _, _) = f0rge(&repo, &["label", "edit", "--name", "nope"]);160     assert_eq!(c, 2, "missing label exits 2");161     // milestones.162     let (c, _, e) = f0rge(&repo, &["milestone", "create", "--name", "v1", "--due", "2025-10-01", "--description", "First", "--json"]);163     assert_eq!(c, 0, "ms create: {e}");164     let (c, o, _) = f0rge(&repo, &["milestone", "list", "--json"]);165     assert_eq!(c, 0);166     assert!(o.contains("\"v1\""));167     let (c, _, _) = f0rge(&repo, &["milestone", "create", "--name", "v1"]);168     assert_eq!(c, 2, "duplicate milestone exits 2");169     let (c, _, e) = f0rge(&repo, &["milestone", "edit", "--name", "v1", "--due", "2025-11-01"]);170     assert_eq!(c, 0, "ms edit: {e}");171     let (c, _, _) = f0rge(&repo, &["milestone", "edit", "--name", "nope"]);172     assert_eq!(c, 2, "missing milestone exits 2");173     let ms = std::fs::read_to_string(repo.join(".forge").join("milestones.md")).unwrap();174     assert!(ms.contains("2025-11-01"));175     let _ = std::fs::remove_dir_all(&repo);176 }