mod common;
use common::{FakeProvider, TestRepo};
use predicates::prelude::PredicateBooleanExt;
fn two_branch_stack() -> TestRepo {
let repo = TestRepo::new();
repo.git(["config", "stk.provider", "github"]);
repo.stack().args(["new", "feature/a"]).assert().success();
repo.commit_file("a.txt", "a\n", "a work");
repo.stack().args(["new", "feature/b"]).assert().success();
repo.commit_file("b.txt", "b\n", "b work");
repo
}
fn graphql_batch(
a_state: &str,
a_queue: &str,
a_reviews: &str,
b_state: &str,
b_queue: &str,
b_reviews: &str,
) -> String {
format!(
r#"{{"data":{{"repository":{{
"p0":{{"nodes":[{{"number":9,"headRefName":"feature/a","mergeQueueEntry":{a_queue},
"latestReviews":{{"nodes":{a_reviews}}},
"commits":{{"nodes":[{{"commit":{{"statusCheckRollup":{{"state":"{a_state}"}}}}}}]}}}}]}},
"p1":{{"nodes":[{{"number":10,"headRefName":"feature/b","mergeQueueEntry":{b_queue},
"latestReviews":{{"nodes":{b_reviews}}},
"commits":{{"nodes":[{{"commit":{{"statusCheckRollup":{{"state":"{b_state}"}}}}}}]}}}}]}}
}}}}}}"#
)
}
fn github_fake(repo: &TestRepo, graphql: &str) -> common::FakeProviderEnv {
FakeProvider::new()
.on("repo view", r#"{"nameWithOwner":"owner/repo"}"#)
.on("api graphql", graphql)
.fallback("[]")
.install(repo)
}
#[test]
fn list_shows_a_status_dot_next_to_the_pr_number() {
let repo = two_branch_stack();
let graphql = graphql_batch("SUCCESS", "null", "[]", "FAILURE", "null", "[]");
let fake = github_fake(&repo, &graphql);
repo.stack_faked(&fake)
.arg("list")
.assert()
.success()
.stdout(predicates::str::contains("🟢 #9"))
.stdout(predicates::str::contains("🔴 #10"));
}
#[test]
fn list_reviews_lists_the_review_tallies_under_each_branch() {
let repo = two_branch_stack();
let graphql = graphql_batch(
"SUCCESS",
"null",
r#"[{"state":"APPROVED"},{"state":"APPROVED"}]"#,
"FAILURE",
"null",
r#"[{"state":"CHANGES_REQUESTED"},{"state":"COMMENTED"}]"#,
);
let fake = github_fake(&repo, &graphql);
repo.stack_faked(&fake)
.args(["list", "--reviews"])
.assert()
.success()
.stdout(predicates::str::contains("2 approvals"))
.stdout(predicates::str::contains("1 requested change"))
.stdout(predicates::str::contains("1 comment"));
}
#[test]
fn list_reviews_marks_a_pr_with_no_reviews() {
let repo = two_branch_stack();
let graphql = graphql_batch("SUCCESS", "null", "[]", "SUCCESS", "null", "[]");
let fake = github_fake(&repo, &graphql);
repo.stack_faked(&fake)
.args(["list", "--reviews"])
.assert()
.success()
.stdout(predicates::str::contains("(no reviews)"));
}
#[test]
fn list_marks_a_pr_that_sits_in_the_merge_queue() {
let repo = two_branch_stack();
let graphql = graphql_batch(
"SUCCESS",
"null",
"[]",
"SUCCESS",
r#"{"state":"QUEUED"}"#,
"[]",
);
let fake = github_fake(&repo, &graphql);
repo.stack_faked(&fake)
.arg("list")
.assert()
.success()
.stdout(predicates::str::contains("🕑 #10"))
.stdout(predicates::str::contains("🟢 #10").not())
.stdout(predicates::str::contains("🟢 #9"));
}
#[test]
fn list_local_makes_no_provider_calls() {
let repo = two_branch_stack();
let fake = FakeProvider::new()
.fallback_fail("no network allowed under --local")
.install(&repo);
repo.stack_faked(&fake)
.args(["list", "--local"])
.assert()
.success()
.stdout(predicates::str::contains("feature/a"))
.stdout(predicates::str::contains("feature/b"))
.stdout(predicates::str::contains("#").not());
}
#[test]
fn list_reviews_conflicts_with_commits_format_and_local() {
let repo = TestRepo::new();
repo.stack().args(["new", "feature/a"]).assert().success();
let conflicting: [&[&str]; 3] = [
&["list", "--reviews", "--commits"],
&["list", "--reviews", "--format", "markdown"],
&["list", "--reviews", "--local"],
];
for args in conflicting {
repo.stack().args(args).assert().failure();
}
}
#[test]
fn status_shows_a_status_dot_for_the_reviews_pr() {
let repo = two_branch_stack();
repo.git(["switch", "feature/a"]);
let fake = FakeProvider::new()
.on(
"pr view 9 --json statusCheckRollup",
r#"{"statusCheckRollup":[{"status":"COMPLETED","conclusion":"SUCCESS"}]}"#,
)
.on(
"--head feature/a",
r##"[{"number":9,"state":"OPEN","baseRefName":"main","headRefName":"feature/a","url":"https://github.com/owner/repo/pull/9","title":"A"}]"##,
)
.fallback("[]")
.install(&repo);
repo.stack_faked(&fake)
.args(["status", "feature/a"])
.assert()
.success()
.stdout(predicates::str::contains("🟢 #9"));
}
#[test]
fn list_and_status_show_a_platform_stack() {
let repo = TestRepo::new();
repo.git(["config", "stk.provider", "github"]);
repo.stack().args(["new", "feature/a"]).assert().success();
repo.commit_file("a.txt", "a\n", "a work");
let graphql = r##"{"data":{"repository":{"p0":{"nodes":[{"number":12,"headRefName":"feature/a",
"mergeQueueEntry":null,"stack":{"number":6,"size":3},"stackEntry":{"position":2},
"commits":{"nodes":[{"commit":{"statusCheckRollup":{"state":"SUCCESS"}}}]}}]}}}}"##;
let stacks = r##"[{"number":6,"open":true,"base":{"ref":"main"},"pull_requests":[
{"number":11,"head":{"ref":"below"}},
{"number":12,"head":{"ref":"feature/a"}},
{"number":13,"head":{"ref":"above"}}]}]"##;
let fake = FakeProvider::new()
.log_all("calls.txt")
.on("api graphql", graphql)
.on("repo view", r##"{"nameWithOwner":"owner/repo"}"##)
.on("repos/owner/repo/stacks", stacks)
.on("feature/a", r##"[{"number":12,"state":"OPEN","baseRefName":"main","headRefName":"feature/a","url":"https://example.com/12","title":"A work"}]"##)
.fallback("[]")
.install(&repo);
repo.stack_faked(&fake)
.args(["list"])
.assert()
.success()
.stdout(predicates::str::contains("⛁2/3"));
std::fs::remove_file(repo.path().join("calls.txt")).ok();
repo.stack_faked(&fake)
.args(["status"])
.assert()
.success()
.stdout(predicates::str::contains("stack: github stack 6 (2 of 3)"));
let calls = std::fs::read_to_string(repo.path().join("calls.txt")).expect("call log");
assert!(
!calls.contains("repos/owner/repo/stacks"),
"status paid a REST listing call for what the annotate query answers:\n{calls}"
);
}
#[test]
fn list_says_so_under_verbose_when_the_host_rejects_the_stack_fields() {
let repo = TestRepo::new();
repo.git(["config", "stk.provider", "github"]);
repo.stack().args(["new", "feature/a"]).assert().success();
repo.commit_file("a.txt", "a\n", "a work");
let without = r##"{"data":{"repository":{"p0":{"nodes":[{"number":12,"headRefName":"feature/a",
"mergeQueueEntry":null,
"commits":{"nodes":[{"commit":{"statusCheckRollup":{"state":"SUCCESS"}}}]}}]}}}}"##;
let fake = FakeProvider::new()
.fail_with_stdout(
"stackEntry",
"",
"Field 'stack' doesn't exist on type 'PullRequest'",
)
.on("api graphql", without)
.on("repo view", r##"{"nameWithOwner":"owner/repo"}"##)
.fallback("[]")
.install(&repo);
repo.stack_faked(&fake)
.args(["list"])
.assert()
.success()
.stderr(predicates::str::contains("stacked-pull-request fields").not())
.stdout(predicates::str::contains("⛁").not());
repo.stack_faked(&fake)
.args(["-v", "list"])
.assert()
.success()
.stderr(predicates::str::contains(
"did not accept the stacked-pull-request fields",
));
}
#[test]
fn list_stays_quiet_when_the_provider_fails_for_another_reason() {
let repo = TestRepo::new();
repo.git(["config", "stk.provider", "github"]);
repo.stack().args(["new", "feature/a"]).assert().success();
repo.commit_file("a.txt", "a\n", "a work");
let fake = FakeProvider::new()
.on("repo view", r##"{"nameWithOwner":"owner/repo"}"##)
.fail("api graphql", "error connecting to api.github.com")
.fallback("[]")
.install(&repo);
repo.stack_faked(&fake)
.args(["list"])
.assert()
.success()
.stdout(predicates::str::contains("feature/a"))
.stderr(predicates::str::contains("stacked-pull-request fields").not());
}
#[test]
fn status_does_not_list_every_review_on_gitlab() {
let repo = TestRepo::new();
repo.git(["config", "stk.provider", "gitlab"]);
repo.stack().args(["new", "feature/a"]).assert().success();
repo.commit_file("a.txt", "a\n", "a work");
let fake = FakeProvider::new()
.log_all("calls.txt")
.fallback(
r##"[{"iid":34,"state":"opened","target_branch":"main","source_branch":"feature/a","web_url":"https://gitlab.com/o/r/-/merge_requests/34"}]"##,
)
.install(&repo);
repo.stack_faked(&fake)
.args(["status", "feature/a"])
.assert()
.success()
.stdout(predicates::str::contains("review: !34 open"));
let calls = std::fs::read_to_string(repo.path().join("calls.txt")).expect("call log");
assert!(
!calls.lines().any(|line| line.contains("mr list")
&& !line.contains("--source-branch")
&& !line.contains("--branch")),
"status listed every open review to annotate one branch:\n{calls}"
);
}
#[test]
fn status_names_the_stack_of_a_merged_review_without_the_open_query() {
let repo = TestRepo::new();
repo.git(["config", "stk.provider", "github"]);
repo.stack().args(["new", "feature/a"]).assert().success();
repo.commit_file("a.txt", "a\n", "a work");
let stacks = r##"[{"number":6,"open":true,"base":{"ref":"main"},"pull_requests":[
{"number":11,"state":"open","head":{"ref":"below"}},
{"number":12,"state":"closed","head":{"ref":"feature/a"}},
{"number":13,"state":"open","head":{"ref":"above"}}]}]"##;
let fake = FakeProvider::new()
.log_all("calls.txt")
.on("repo view", r##"{"nameWithOwner":"owner/repo"}"##)
.on("repos/owner/repo/stacks", stacks)
.on("feature/a --state merged", r##"[{"number":12,"state":"MERGED","baseRefName":"main","headRefName":"feature/a","url":"https://example.com/12","title":"A work"}]"##)
.on("feature/a", "[]")
.fallback("[]")
.install(&repo);
repo.stack_faked(&fake)
.args(["status"])
.assert()
.success()
.stdout(predicates::str::contains("stack: github stack 6 (2 of 3)"));
let calls = std::fs::read_to_string(repo.path().join("calls.txt")).expect("call log");
assert!(
!calls.contains("api graphql"),
"the annotate query asks states:OPEN and cannot match a merged review:\n{calls}"
);
}