Skip to content
Merged
4 changes: 3 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,9 @@ on, not whether those dependencies are satisfied, and beads already answers
that. `bd swarm status` shows Ready as a first-class state alongside Completed,
Active and Blocked, so a viewer that collapsed Ready into plain "open" would be
throwing away a distinction beads makes. One call each per project, intersected
with the tree's ids. bd reads no edge to another project's bead, and that is
with the tree's ids. A bare `bd ready` leaves gates out as work nobody claims,
so **`bd ready --type gate --limit 0 --json`** is asked beside it, and a gate
with no open blocker is ready like any other bead. bd reads no edge to another project's bead, and that is
the one place `bdi` adds to its answer: see *Across projects*.

**`bd list --all --include-gates --limit 0 --json` and `bd query
Expand Down
45 changes: 38 additions & 7 deletions src/collect/bd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,12 +733,20 @@ impl Tracker for Reader<'_> {

/// bd computes readiness itself and treats it as a state of its own, so
/// it is asked for rather than inferred from status.
///
/// A bare `bd ready` leaves gates out as work nobody claims, and answers
/// for them by the same rule when asked by type, so gates are asked for
/// beside it.
fn ready(&self) -> Result<BTreeSet<String>, RunFailure> {
let out = self.asked(&["ready", "--limit", "0", "--json"])?;
Ok(rows(&out, "ready")?
.into_iter()
.map(|bead| bead.id)
.collect())
let (work, gates) = together(
|| self.asked(&["ready", "--limit", "0", "--json"]),
|| self.asked(&["ready", "--type", "gate", "--limit", "0", "--json"]),
);
let mut ready = BTreeSet::new();
for out in [work?, gates?] {
ready.extend(rows(&out, "ready")?.into_iter().map(|bead| bead.id));
}
Ok(ready)
}

/// A dep-tree row carries its tree parent, not its blocker set: a bead
Expand Down Expand Up @@ -1536,7 +1544,9 @@ mod tests {
/// `bdi` was launched from holds, which the adapter captured once.
#[test]
fn a_project_configuring_nothing_is_read_on_the_ambient_credential() {
let runner = FakeRunner::default().with(&spelled("ready --limit 0 --json"), "[]");
let runner = FakeRunner::default()
.with(&spelled("ready --limit 0 --json"), "[]")
.with(&spelled("ready --type gate --limit 0 --json"), "[]");

let cli = launched_with(&runner, Some("hunter2"));
let tracker = cli
Expand Down Expand Up @@ -2209,7 +2219,9 @@ mod tests {
fn ready_ids_returns_the_set_bd_considers_startable() {
let out = r#"[{"id":"p-1.1","title":"a","status":"open"},
{"id":"p-1.3","title":"b","status":"open"}]"#;
let runner = FakeRunner::default().with(&spelled("ready --limit 0 --json"), out);
let runner = FakeRunner::default()
.with(&spelled("ready --limit 0 --json"), out)
.with(&spelled("ready --type gate --limit 0 --json"), "[]");

let got = opened(&runner).ready().unwrap();

Expand All @@ -2221,6 +2233,25 @@ mod tests {
);
}

/// A bare `bd ready` leaves gates out as work nobody claims, and lists
/// them when asked for by type, by the same rule as any other bead.
#[test]
fn a_gate_bd_lists_when_asked_for_gates_is_ready() {
let runner = FakeRunner::default()
.with(
&spelled("ready --limit 0 --json"),
r#"[{"id":"p-1.1","title":"a","status":"open"}]"#,
)
.with(
&spelled("ready --type gate --limit 0 --json"),
r#"[{"id":"p-wg1","title":"Gate: gh:pr","status":"open","issue_type":"gate"}]"#,
);

let got = opened(&runner).ready().unwrap();

assert_eq!(got, BTreeSet::from(["p-1.1".into(), "p-wg1".into()]));
}

/// The shape a real tracker produces: a bead blocked by two beads, whose
/// dep-tree row names only one of them.
#[test]
Expand Down
79 changes: 79 additions & 0 deletions tests/an_open_gate_with_no_open_blocker_is_ready.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! An open gate with no open blocker is as ready as any other bead, in the
//! listing `bdi --beads` writes and on its row of the tree.
//!
//! A bare `bd ready` leaves gates out, as work nobody claims, so the shim
//! answers the two questions apart the way bd does: the bare one with no
//! gate, and the one asked for gates with the gates that have no open
//! blocker.

mod terminal;

use std::path::Path;
use std::process::Command;

use serde_json::Value;
use terminal::a_home_naming_one_project;
use terminal::shims::ShimmedTracker;

/// Three beads each blocked by a `gh:pr` gate, as bd 1.3.0 wrote them.
const GH_PR_GATES: &str = include_str!("fixtures/bd_1.3.0_gh_pr_gates.json");

/// What bd says is ready when asked for gates: all three, since none of
/// them waits on anything.
const READY_GATES: &str = r#"[
{"id":"dun-w8h","title":"Gate: gh:pr","status":"open","issue_type":"gate"},
{"id":"dun-5s6","title":"Gate: gh:pr","status":"open","issue_type":"gate"},
{"id":"dun-auk","title":"Gate: gh:pr","status":"open","issue_type":"gate"}
]"#;

/// What a one-shot `bdi` run in `home` with `args` wrote, read as JSON.
fn one_shot(home: &Path, tracker: &ShimmedTracker, args: &[&str]) -> Value {
let out = Command::new(env!("CARGO_BIN_EXE_bdi"))
.args(args)
.current_dir(home)
.env("HOME", home)
.env_remove("BEADS_DIR")
.env_remove("BEADS_DOLT_PASSWORD")
.env_remove("BDI_PROJECT")
.envs(tracker.environment())
.output()
.expect("bdi runs");
assert!(
out.status.success(),
"bdi exited {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_slice(&out.stdout).expect("bdi writes JSON")
}

fn ready<'a>(beads: impl IntoIterator<Item = &'a Value>, id: &str) -> &'a Value {
&beads
.into_iter()
.find(|bead| bead["id"] == id)
.unwrap_or_else(|| panic!("{id} is listed"))["ready"]
}

#[test]
fn an_open_gate_with_no_open_blocker_is_ready_in_the_listing_and_on_its_row() {
let home = a_home_naming_one_project("ready-gate");
let tracker = ShimmedTracker::beside(&home);
tracker.holds(GH_PR_GATES);
tracker.answers_with("ready --type gate --limit 0 --json", READY_GATES);

let listing = one_shot(&home, &tracker, &["--beads"]);
let forest = one_shot(&home, &tracker, &["--json", "--all"]);

assert_eq!(tracker.unanswered(), Vec::<String>::new());
let listed = listing["beads"].as_array().expect("beads");
let rows: Vec<&Value> = forest["trees"]
.as_array()
.expect("trees")
.iter()
.flat_map(|tree| tree["nodes"].as_array().expect("nodes"))
.collect();
for (id, is_ready) in [("dun-w8h", true), ("dun-5s6", true), ("dun-4ga", false)] {
assert_eq!(ready(listed, id), is_ready, "{id} in the listing");
assert_eq!(ready(rows.iter().copied(), id), is_ready, "{id} on its row");
}
}
1 change: 1 addition & 0 deletions tests/terminal/shims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ impl ShimmedTracker {
answer("query ephemeral=true --limit 0 --json", &[]);
answer("query ephemeral=true --all --limit 0 --json", &[]);
answer("ready --limit 0 --json", &[]);
answer("ready --type gate --limit 0 --json", &[]);
answer("blocked --json", &[]);
}

Expand Down