diff --git a/docs/configuration.md b/docs/configuration.md index 2359c6a..1b6f3fe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -153,6 +153,13 @@ could be read from. to the values a pattern matches, and the pattern is anchored against the whole value: `human` draws on `human` and not on `inhumane`. +**Several entries may name one key, and they are tried in the order you wrote +them.** The first whose `match` reads the value is the badge the row draws, and +the ones below it are never tried. So write the shape you expect first and the +shapes you will settle for under it. A value none of them reads draws nothing, +and [What a badge says when it cannot do what you asked](#what-a-badge-says-when-it-cannot-do-what-you-asked) +has that case. + A capture the pattern names is `render`'s to place by that name: ```toml @@ -264,16 +271,28 @@ colour = "#c71585" ## `[[projects.badges]]` -What one project draws in place of this list, for the keys it names and no -others. A shared list can only say what the value itself carries, so a project -whose `delivery_pr` leaves out its owner and repository needs an entry of its -own to supply them. Every key a project stays silent about keeps drawing what +What one project draws ahead of this list, for the keys it names and no others. +A shared list can only say what the value itself carries, so a project whose +`delivery_pr` leaves out its owner and repository needs an entry of its own to +supply them. Every key a project stays silent about keeps drawing what `[[badges]]` says. -It shadows by key rather than by entry: a project naming `blocked_on` replaces -*every* `[[badges]]` entry for `blocked_on`, however many values they match -between them. Its entries stand where the first `[[badges]]` entry for that key -stood, so overriding one badge does not reorder the row. +**A project's entries for a key are tried before the `[[badges]]` entries for +that key, and replace none of them.** They sit where the first `[[badges]]` entry +for the key sat, so naming a key does not move the row's other badges. The shared +entries for that key follow, in the order they were written. Keys only the +project names come last. + +So a project wins a value by being tried first, not by taking anything away. A +value its own entries do not read falls through to the `[[badges]]` entries +underneath them, and draws whatever it would have drawn had the project named +nothing. + +That is why one entry is usually all a project needs. It names the shape its own +tracker writes, and every other shape keeps being read by the shared list. + +A project cannot silence a `[[badges]]` entry. Naming a key puts your own entries +first; it does not take the shared ones away. **A project's own keys have to come before its badges.** `[[projects.badges]]` opens a table of its own, so `name`, `path` or anything else written after it @@ -355,7 +374,7 @@ and the badge drops the repository rather than the row dropping the badge, and A bead carrying `delivery_pr = "12"` has nothing in the value to build an address out of, and a shared list can only say what the value itself carries. -The project's own entry supplies the rest: +The project's own entry supplies the rest, and is tried before the shared one: ```toml [[projects]] @@ -369,8 +388,10 @@ render = "⇢ #{number}" link = "https://forge.invalid/orbital/beacon/pull/{number}" ``` -This shadows `delivery_pr` for `beacon` and for nothing else, by the rule -[`[[projects.badges]]`](#projectsbadges) gives. +This is tried first on `beacon`'s beads, and on no other project's, by the rule +[`[[projects.badges]]`](#projectsbadges) gives. The `[[badges]]` entry is still +there underneath it, so a `beacon` bead that does carry an owner and repository +is read by that one as before. ### A reference stored as a full URL @@ -388,16 +409,21 @@ link = "{}" ### What a badge says when it cannot do what you asked -A badge with no `link` is a filter. It is written to decline, so a value its -pattern does not read draws nothing and reports nothing. +`match` is how you say which values you want, so a badge whose pattern does not +read the value draws nothing and reports nothing. The next entry for that key is +tried instead. A value no entry for the key reads stays silent: you said which +shapes you wanted, and none of them was this one. Where you would rather see +every value of a key, write a last entry with a permissive pattern, and it draws +whatever the ones above it declined. -A badge with a `link` is written to point somewhere, so a value it cannot point -at is a reference the reader has lost. The row says so where it says its -anomalies: +What is worth saying is a badge that read the value and then could not keep one +of the other promises its config made. A `link` was written to point somewhere +and a `short` to survive a narrow pane, and a value that defeats either takes +that away while leaving the badge looking ordinary. So the row says so where it +says its anomalies: | what the badge met | what the row says | |---|---| -| a value no pattern reads | `no badge for delivery_pr: no pattern reads this value` | | a value that left part of the `link` unfilled | `no link for delivery_pr: this value leaves part of it unfilled` | | a link holding a control character | `no link for delivery_pr: it holds a control character` | | a value that left part of the `short` unfilled | `no short form for delivery_pr: this value leaves part of it unfilled` | diff --git a/src/config.rs b/src/config.rs index 0b05024..d8160cb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -661,12 +661,13 @@ impl Config { } /// The badges this project draws: the global list, with a project's own - /// entries standing in for every global entry that shares a key with one - /// of them. + /// entries for a key ahead of the global entries for that same key. /// - /// A shadowed key's entries stand where the global list's first entry for - /// that key stood, so overriding one badge does not reorder the row. Keys - /// only the project names follow the rest. + /// A project wins a value by being tried first rather than by replacing + /// anything, so a value its own entries do not read falls through to the + /// shared shapes. Its entries stand where the global list's first entry for + /// that key stood, so naming a key does not reorder the row. Keys only the + /// project names follow the rest. pub fn badges_for_project(&self, project: &str) -> Vec { let Some(own) = self .projects @@ -678,20 +679,16 @@ impl Config { return self.badges.clone(); }; let mut drawn: Vec = Vec::new(); - let mut stood_in_for: BTreeSet<&str> = BTreeSet::new(); + let mut went_ahead_of: BTreeSet<&str> = BTreeSet::new(); for global in &self.badges { - match own.iter().any(|b| b.key == global.key) { - false => drawn.push(global.clone()), - true => { - if stood_in_for.insert(&global.key) { - drawn.extend(own.iter().filter(|b| b.key == global.key).cloned()); - } - } + if own.iter().any(|b| b.key == global.key) && went_ahead_of.insert(&global.key) { + drawn.extend(own.iter().filter(|b| b.key == global.key).cloned()); } + drawn.push(global.clone()); } drawn.extend( own.iter() - .filter(|b| !stood_in_for.contains(b.key.as_str())) + .filter(|b| !went_ahead_of.contains(b.key.as_str())) .cloned(), ); drawn @@ -1198,7 +1195,7 @@ path = "/home/user/dev/cinder" } #[test] - fn a_projects_badge_stands_where_the_global_one_it_shadows_stood() { + fn a_projects_badge_stands_where_the_first_global_one_for_its_key_stood() { let cfg = Config { badges: vec![ badge("metadata.delivery_pr", "⇢ {}"), @@ -1217,6 +1214,7 @@ path = "/home/user/dev/cinder" cfg.badges_for_project("beacon"), vec![ badge("metadata.delivery_pr", "⇢ beacon/{}"), + badge("metadata.delivery_pr", "⇢ {}"), matching("metadata.blocked_on", "human", "⏸ waiting"), badge("metadata.epic", "▣ {}"), ] @@ -1224,11 +1222,15 @@ path = "/home/user/dev/cinder" } /// The global list may name one key several times, matched on a different - /// value each time. A project overriding that key replaces the whole group - /// rather than one of its entries: shadowing half a key would leave the - /// project drawing the shared wording for every value it did not name. + /// value each time. A project naming that key is tried ahead of the whole + /// group and replaces none of it, so the project says what it wants for the + /// values it names and keeps the shared wording for the rest. + /// + /// One entry ahead of two is what makes the precedence visible: the project + /// draws its own words for `human`, and `dependency` still reaches the + /// shared entry that reads it. #[test] - fn a_projects_badge_shadows_every_global_entry_for_its_key() { + fn a_projects_badge_is_tried_ahead_of_every_global_entry_for_its_key() { let cfg = Config { badges: vec![ matching("metadata.blocked_on", "human", "⏸ waiting"), @@ -1242,7 +1244,11 @@ path = "/home/user/dev/cinder" assert_eq!( cfg.badges_for_project("beacon"), - vec![matching("metadata.blocked_on", "human", "⏸ ask Ada")] + vec![ + matching("metadata.blocked_on", "human", "⏸ ask Ada"), + matching("metadata.blocked_on", "human", "⏸ waiting"), + matching("metadata.blocked_on", "dependency", "⏸ blocked"), + ] ); } diff --git a/src/model/badges.rs b/src/model/badges.rs index 80029d9..aa642d1 100644 --- a/src/model/badges.rs +++ b/src/model/badges.rs @@ -5,6 +5,8 @@ //! what to say for a value, are both config's to state; this renders what it //! is given and knows no more about `blocked_on` than about any other key. +use std::collections::BTreeSet; + use serde::Serialize; use crate::config::{Badge, Colour}; @@ -35,15 +37,13 @@ pub struct Badged { /// A badge that drew less than its config asked for. /// -/// A badge with no `link` is a filter: it is written to decline, so a value -/// its pattern does not match is nothing to report and reaches neither of the -/// first two. A badge with a `link` is written to point somewhere, so a value -/// it cannot point at is a reference the reader has lost. +/// Every badge here read the value and then could not keep a promise its +/// config made about the badge it drew. A pattern that does not read the value +/// is a filter declining, which is what patterns are for, so it is nowhere +/// here however many of them decline in a row. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(tag = "undrawn", rename_all = "kebab-case")] pub enum Undrawn { - /// No badge at all: the pattern does not read this bead's value. - Badge { key: String }, /// A badge without its link: the template named a capture this value did /// not supply, and a destination built round a part that was never there /// points somewhere else. @@ -71,26 +71,27 @@ pub struct Badges { pub fn badges_for(bead: &Bead, badges: &[Badge]) -> Badges { let mut drawn = Vec::new(); let mut undrawn = Vec::new(); + let mut read: BTreeSet<&str> = BTreeSet::new(); for badge in badges { let Some(value) = bead.values.get(&badge.key).map(String::as_str) else { continue; }; - // A badge with no `link` never reports: whatever it does with this - // value, it did what its config asked. - let promised = badge.link.is_some(); + // Badges on one key are a chain read in config order, so the entries + // below the one that read this value are what the reader wrote for the + // values it does not read. They are not tried at all, and neither is + // what their config promised about a value they were never given. + if read.contains(badge.key.as_str()) { + continue; + } let Some(text) = badge.apply(value) else { - if promised { - undrawn.push(Undrawn::Badge { - key: badge.key.clone(), - }); - } continue; }; + read.insert(badge.key.as_str()); let link = badge.link_for(value); - if promised && link.is_none() { + if badge.link.is_some() && link.is_none() { undrawn.push(Undrawn::Link { key: badge.key.clone(), }); @@ -337,11 +338,67 @@ mod tests { } } + // ---- several badges on one key --------------------------------------- + + /// Ordering is what a list of badges on one key says: each is tried until + /// one reads the value, so the qualified form above a permissive entry + /// draws the qualified form and the entry below it stands in for nothing. + /// + /// The permissive entry names a `short` its own pattern cannot fill, which + /// is something running it would have had to report. Its silence is how + /// this asserts it never ran at all. + #[test] + fn two_badges_on_one_key_that_both_read_a_value_draw_the_first_alone() { + let bead = bead_with(r#"{"delivery_pr":"orbital/atlas#30"}"#); + let permissive = Badge { + match_value: Some(matching(".*")), + render: "⇢ {}".into(), + short: Some("⇢ {repo}".into()), + link: None, + ..qualified_only() + }; + + let got = badges_for(&bead, &[qualified_only(), permissive]); + + assert_eq!( + got.drawn, + vec![Badged { + key: "metadata.delivery_pr".to_string(), + text: "⇢ #30".to_string(), + short: None, + link: Some("https://forge.invalid/orbital/atlas/pull/30".to_string()), + colour: None, + }] + ); + assert_eq!(got.undrawn, Vec::new()); + } + + /// A value every badge on its key declined is a value nothing was written + /// to read, and that is the reader's list saying what it wanted rather than + /// anything falling short. A `link` on the badges that declined is a + /// promise about where a badge points and not about which values reach one. + #[test] + fn a_value_no_badge_on_its_key_reads_draws_nothing_and_reports_nothing() { + let bead = bead_with(r#"{"delivery_pr":"30"}"#); + let url_only = Badge { + match_value: Some(matching( + r"https://forge\.invalid/[^/]+/(?[^/]+)/pull/(?[0-9]+)", + )), + ..qualified_only() + }; + + let got = badges_for(&bead, &[qualified_only(), url_only]); + + assert_eq!(got.drawn, Vec::new()); + assert_eq!(got.undrawn, Vec::new()); + } + // ---- what a badge meant to draw could not draw ----------------------- /// The pattern a global list writes for a `delivery_pr` reads the /// qualified form. A tracker holding a bare number as well has beads this - /// pattern cannot read at all, and dropping them tells the reader nothing. + /// pattern cannot read at all, and a permissive entry below it is how a + /// reader asks to see them. fn qualified_only() -> Badge { Badge { key: "metadata.delivery_pr".into(), @@ -355,24 +412,10 @@ mod tests { } } - #[test] - fn a_badge_meant_to_draw_reports_a_value_its_pattern_cannot_read() { - let bead = bead_with(r#"{"delivery_pr":"30"}"#); - - let got = badges_for(&bead, &[qualified_only()]); - - assert_eq!(got.drawn, Vec::new()); - assert_eq!( - got.undrawn, - vec![Undrawn::Badge { - key: "metadata.delivery_pr".to_string() - }] - ); - } - - /// The other half of the rule, and the half the shipped waiting badge - /// rests on: a config that names no `link` is a filter, and declining is - /// what it was written to do. + /// The same silence read off the badge the shipped waiting badge is written + /// as, where the one above is read off a pair that name a `link`: a pattern + /// is how a config says which values it wants, and declining is what every + /// pattern is there to do. #[test] fn a_badge_configured_to_decline_stays_silent() { let bead = bead_with(r#"{"blocked_on":"dependency"}"#); diff --git a/src/model/snapshot/build.rs b/src/model/snapshot/build.rs index a6fd438..9b221aa 100644 --- a/src/model/snapshot/build.rs +++ b/src/model/snapshot/build.rs @@ -191,7 +191,7 @@ mod tests { use crate::collect::bd::parse_beads; use crate::config::Scope; use crate::model::anomaly::Anomaly; - use crate::model::badges::{Badged, Undrawn}; + use crate::model::badges::Badged; use crate::model::edges::relations; use crate::model::join::{AgentRef, BeadKey, Conflict, JoinSource, Listed}; use crate::model::snapshot::tests::*; @@ -487,10 +487,11 @@ mod tests { /// qualified form cannot read: a bare number with no repository to build /// a URL out of, and a URL held whole where the parts were expected. /// - /// Both used to leave the tree carrying nothing at all, which is the - /// silence this reports instead. + /// The config asked for the qualified form and got its answer, so the two + /// it declined are silent the whole way through the build rather than only + /// at the badge. A reader wanting them writes a second entry for the key. #[test] - fn a_value_the_badge_cannot_read_reaches_the_node_as_undrawn() { + fn a_value_no_badge_on_its_key_reads_reaches_the_node_drawing_nothing() { let json = r#"[ {"id":"orb-8","title":"root","status":"open"}, {"id":"orb-8.1","title":"a bare number","status":"blocked", @@ -530,13 +531,10 @@ link = "https://forge.invalid/{owner}/{repo}/pull/{number}" now(), ); - let unread = Undrawn::Badge { - key: "metadata.delivery_pr".to_string(), - }; - assert_eq!(node(&t, "orb-8.1").undrawn, vec![unread.clone()]); - assert!(node(&t, "orb-8.1").badges.is_empty()); - assert_eq!(node(&t, "orb-8.2").undrawn, vec![unread]); - assert!(node(&t, "orb-8.2").badges.is_empty()); + for unread in ["orb-8.1", "orb-8.2"] { + assert!(node(&t, unread).badges.is_empty(), "drew on {unread}"); + assert_eq!(node(&t, unread).undrawn, Vec::new(), "reported on {unread}"); + } assert_eq!( node(&t, "orb-8.3").undrawn, @@ -561,10 +559,14 @@ link = "https://forge.invalid/{owner}/{repo}/pull/{number}" ); } - /// A project's own entry for a key is what its beads draw, and the global - /// entry for that key never reaches the tree. + /// A project's own entry for a key is tried before the shared entries for + /// it, so a value both read draws the project's words. + /// + /// Where the two rules meet: the shared entry is still in the list and + /// still reads this value, and what keeps it off the row is only that the + /// project's entry was tried first and stopped the chain. #[test] - fn a_projects_own_badge_reaches_its_nodes_in_place_of_the_global_one() { + fn a_projects_own_badge_draws_the_value_a_global_one_would_have() { let assembled = assembled(BEADS); let panes = panes(PANES); let joined = joined(&assembled.beads, &panes); @@ -611,6 +613,80 @@ render = "⏸ waiting" ); } + /// The bare-number case `docs/configuration.md` works through, and what + /// precedence buys over replacing the shared entries: the project supplies + /// the shape its own tracker writes, and a bead written in the shared shape + /// is still read by the shared entry underneath it. + /// + /// Neither pattern reads the other's value, so each bead names which entry + /// drew it. A project that replaced the shared entry rather than going in + /// front of it would leave the second bead with no badge at all. + #[test] + fn a_value_a_projects_badge_does_not_read_falls_through_to_the_shared_one() { + let json = r#"[ + {"id":"orb-9","title":"root","status":"open"}, + {"id":"orb-9.1","title":"a bare number","status":"blocked", + "metadata":{"delivery_pr":"12"}, + "dependencies":[{"depends_on_id":"orb-9","type":"parent-child"}]}, + {"id":"orb-9.2","title":"the shape the shared list reads","status":"blocked", + "metadata":{"delivery_pr":"orbital/atlas#30"}, + "dependencies":[{"depends_on_id":"orb-9","type":"parent-child"}]} + ]"#; + let cfg = Config::from_toml( + r#" +[[projects]] +name = "orbital" +path = "/srv/work/orbital" + +[[projects.badges]] +key = "metadata.delivery_pr" +match = "(?[0-9]+)" +render = "⇢ #{number}" + +[[badges]] +key = "metadata.delivery_pr" +match = "(?[^/]+)/(?[^#]+)#(?[0-9]+)" +render = "⇢ {repo} #{number}" +"#, + ) + .expect("the config parses"); + let beads = parse_beads(json).expect("the rows parse"); + + let t = build_tree( + "orbital", + &assembled(json), + &Joined::default(), + &Readiness::default(), + &relations(&beads), + ProviderState::Answering, + &cfg, + now(), + ); + + assert_eq!( + node(&t, "orb-9.1").badges, + vec![Badged { + key: "metadata.delivery_pr".to_string(), + text: "⇢ #12".to_string(), + link: None, + short: None, + colour: None, + }], + "the project's own entry is the one that reads a bare number" + ); + assert_eq!( + node(&t, "orb-9.2").badges, + vec![Badged { + key: "metadata.delivery_pr".to_string(), + text: "⇢ atlas #30".to_string(), + link: None, + short: None, + colour: None, + }], + "the shared entry still reads the shape the project said nothing about" + ); + } + #[test] fn the_agent_reaches_the_node_with_the_direction_that_resolved_it() { let t = tree(); diff --git a/src/view/draw/bead.rs b/src/view/draw/bead.rs index 07e9621..61ab251 100644 --- a/src/view/draw/bead.rs +++ b/src/view/draw/bead.rs @@ -685,7 +685,7 @@ mod tests { fn a_badge_that_fell_short_of_its_config_says_so_on_the_drawn_row() { let mut short = row(&node("smt-4kd3p.20", "a bead", Status::Blocked)); short.notes = vec![ - phrase::undrawn(&Undrawn::Badge { + phrase::undrawn(&Undrawn::Link { key: "delivery_pr".into(), }), phrase::unopenable_link("jira"), diff --git a/src/view/phrase.rs b/src/view/phrase.rs index da7c225..ea0bfc0 100644 --- a/src/view/phrase.rs +++ b/src/view/phrase.rs @@ -705,7 +705,6 @@ pub fn unrecognised_status(status: &Status) -> Option { /// the config that wrote the badge, or the beads that hold the value. pub fn undrawn(undrawn: &Undrawn) -> String { match undrawn { - Undrawn::Badge { key } => format!("no badge for {key}: no pattern reads this value"), Undrawn::Link { key } => { format!("no link for {key}: this value leaves part of it unfilled") } @@ -2004,14 +2003,11 @@ mod tests { assert!(said.contains("triage"), "{said}"); } - /// Each of the five names the key, because the key is the one thing that + /// Each of the four names the key, because the key is the one thing that /// takes the reader to the config or the beads they have to change. They /// differ in what is missing, because each has a different repair. #[test] fn every_word_for_a_badge_that_fell_short_names_its_key() { - let unread = undrawn(&Undrawn::Badge { - key: "delivery_pr".into(), - }); let unfilled = undrawn(&Undrawn::Link { key: "delivery_pr".into(), }); @@ -2021,7 +2017,7 @@ mod tests { let refused = unopenable_link("delivery_pr"); let refused_short = unopenable_short("delivery_pr"); - let mut every = vec![&unread, &unfilled, &unshortened, &refused, &refused_short]; + let mut every = vec![&unfilled, &unshortened, &refused, &refused_short]; for said in &every { assert!(said.contains("delivery_pr"), "{said}"); } @@ -2029,7 +2025,7 @@ mod tests { let said = every.len(); every.sort_unstable(); every.dedup(); - assert_eq!(said, every.len(), "two of the five read alike: {every:?}"); + assert_eq!(said, every.len(), "two of the four read alike: {every:?}"); } #[test] diff --git a/src/view/row.rs b/src/view/row.rs index 4ce6ddf..1cc6cd0 100644 --- a/src/view/row.rs +++ b/src/view/row.rs @@ -445,24 +445,8 @@ mod tests { node } - /// The model's half: a badge that drew nothing at all, because no pattern - /// read the value it was given. - #[test] - fn a_value_no_pattern_reads_leaves_its_key_on_the_row() { - let mut unread = node("smt-4kd3p.20", Status::Open); - unread.undrawn = vec![Undrawn::Badge { - key: "delivery_pr".into(), - }]; - - let row = cells(&unread, ROOT, None, None); - - assert!( - row.notes.iter().any(|note| note.contains("delivery_pr")), - "{row:?}" - ); - } - - /// And a badge that drew, losing only the destination its config named. + /// The model's half: a badge that drew, losing only the destination its + /// config named. #[test] fn a_link_the_value_could_not_fill_leaves_its_key_on_the_row() { let mut unfilled = node("smt-4kd3p.20", Status::Open); diff --git a/tests/two_badges_on_one_key_draw_only_the_first_that_reads_the_value.rs b/tests/two_badges_on_one_key_draw_only_the_first_that_reads_the_value.rs new file mode 100644 index 0000000..6be758e --- /dev/null +++ b/tests/two_badges_on_one_key_draw_only_the_first_that_reads_the_value.rs @@ -0,0 +1,111 @@ +//! Two badges on one key, and the one below never runs: the row reaches the +//! terminal carrying what the first badge to read the value drew, and nothing +//! the second would have drawn from the same value. +//! +//! Driven through the binary because a live config is what exercises this. The +//! model can be asked which badges a bead came to, but what a reader sees is a +//! row, and the choice between two entries on one key is invisible in a row +//! that carries one badge either way — only a screen holding one and not the +//! other says which entry was read. +//! +//! The bead's second key is the other half of the rule, and the half that used +//! to speak: a value no entry for its key reads draws nothing and says nothing. +//! The needle for that is the key's own name, because the sentence it used to +//! say named the key and nothing else on this screen does. Weakening the badge +//! into one that reads the value and then cannot fill its `link` puts the name +//! back on the screen, so the absence is the rule holding rather than the needle +//! being unreachable. +//! +//! Every needle is one word with no space in it. A repaint reaches the wire a +//! word at a time, so a needle spanning a space is one a cursor move can split +//! and a passing test can miss. + +mod terminal; + +use std::time::Duration; + +use terminal::driver::{Driven, GIVING_UP}; +use terminal::shims::ShimmedTracker; +use terminal::{a_home_naming_one_project_settled, contains, ENTER_ALTERNATE_SCREEN}; + +const ROWS: u16 = 40; +const COLS: u16 = 120; + +/// A gap this long between bytes means the collection is over and drawn. +const A_SILENCE: Duration = Duration::from_millis(300); + +/// One bead carrying two references. `delivery_pr` is written in the shape the +/// first badge on that key was written for, so the entry below it is the one +/// that must not run. `jira` is written in a shape no badge on its key reads, +/// which is the value that has to leave the row silent. +const THE_TRACKER: &str = r#"[ + {"id":"orb-1","title":"lift the ground station","status":"open", + "priority":1,"issue_type":"epic"}, + {"id":"orb-1.1","title":"repoint the dish","status":"blocked","parent":"orb-1", + "dependencies":[{"depends_on_id":"orb-1","type":"parent-child"}], + "priority":2,"issue_type":"task", + "metadata":{"delivery_pr":"orbital/atlas#12","jira":"a note to self"}} +]"#; + +/// A key read from the shape expected down to the shape settled for, and a key +/// read in one shape only. +/// +/// The permissive entry names no `match`, which is the most permissive form +/// there is, and renders the whole value — so what it would draw is the value +/// itself and there is nothing else on the screen it could be mistaken for. +/// +/// The `jira` entry names a `link`, because a badge promising somewhere to go +/// is the one with the most to report if anything is still reporting. +const A_CHAIN_AND_A_LONE_ENTRY: &str = concat!( + "\n[[badges]]\n", + "key = \"metadata.delivery_pr\"\n", + "match = \"(?[^/]+)/(?[^#]+)#(?[0-9]+)\"\n", + "render = \"⇢{repo}#{number}\"\n", + "\n[[badges]]\n", + "key = \"metadata.delivery_pr\"\n", + "render = \"{}\"\n", + "\n[[badges]]\n", + "key = \"metadata.jira\"\n", + "match = \"(?[A-Z]+-[0-9]+)\"\n", + "render = \"{ticket}\"\n", + "link = \"https://jira.invalid/browse/{ticket}\"\n", +); + +/// Every tree rather than only the staffed ones, then down onto the tree and +/// open it, so the bead carrying the badges is on the screen. +const OPEN_THE_TREE: &[u8] = b"agjlgj"; + +/// What the first entry for the key draws, which is the row arriving at all. +const THE_SHAPE_IT_WAS_WRITTEN_FOR: &[u8] = "⇢atlas#12".as_bytes(); + +/// What the entry below it would draw from the same value. +const THE_WHOLE_VALUE: &[u8] = b"orbital/atlas#12"; + +/// The key a value no badge reads belongs to, which is what the retired +/// sentence named. +const THE_UNREAD_KEY: &[u8] = b"metadata.jira"; + +#[test] +fn a_key_draws_the_first_badge_that_reads_it_and_stays_silent_where_none_does() { + let home = a_home_naming_one_project_settled("fallthrough", A_CHAIN_AND_A_LONE_ENTRY); + let tracker = ShimmedTracker::beside(&home); + tracker.holds(THE_TRACKER); + + let mut bdi = Driven::bdi(ROWS, COLS, home.clone(), &tracker.environment()); + bdi.read_until(ENTER_ALTERNATE_SCREEN, GIVING_UP); + bdi.settle(A_SILENCE, GIVING_UP); + bdi.send(OPEN_THE_TREE); + + bdi.read_until(THE_SHAPE_IT_WAS_WRITTEN_FOR, GIVING_UP); + bdi.settle(A_SILENCE, GIVING_UP); + + let drawn = bdi.everything(); + assert!( + !contains(&drawn, THE_WHOLE_VALUE), + "the entry below the one that read the value drew as well" + ); + assert!( + !contains(&drawn, THE_UNREAD_KEY), + "a value no badge on its key reads still reported" + ); +}