tests/abbrev_test.rs
Ref: Size: 29.3 KiB History
//! The id-display policy, end to end.
//!
//! The unit tests in `src/abbrev.rs` cover the width arithmetic. These cover
//! the two promises the CLI makes about it: an id it prints can always be
//! typed straight back in, and a prefix that names more than one object is
//! refused rather than resolved.
//!
//! The `web ui` and `dashboard` sections at the bottom hold the same promises
//! to the other two surfaces, which have their own way of getting them wrong:
//! both render a *filtered* subset, and a width computed from what is on
//! screen is not the width the policy asks for.
mod common;
use common::{ServerHarness, TestRepo};
/// Pull the leading id column out of a `list` line.
fn first_column(line: &str) -> &str {
line.split_whitespace().next().unwrap_or("")
}
fn open_issues(repo: &TestRepo, n: usize) -> Vec<String> {
(0..n)
.map(|i| {
let title = format!("issue {}", i);
let out = repo.run_ok(&["issue", "open", "-t", &title]);
out.trim()
.rsplit(' ')
.next()
.expect("open should print an id")
.to_string()
})
.collect()
}
/// The floor stays at 8, so nothing that reads today's output changes.
#[test]
fn a_small_repo_still_prints_eight_character_ids() {
let repo = TestRepo::new("Alice", "alice@example.com");
open_issues(&repo, 3);
let out = repo.run_ok(&["issue", "list"]);
for line in out.lines().filter(|l| !l.trim().is_empty()) {
assert_eq!(
first_column(line).len(),
8,
"expected an 8-character id in: {}",
line
);
}
}
/// Anything printed must be typeable. This is the whole contract: a displayed
/// id is a reference someone will paste back, into a command or a commit
/// message, and it has to work when they do.
#[test]
fn every_id_printed_by_list_resolves_on_its_own() {
let repo = TestRepo::new("Alice", "alice@example.com");
open_issues(&repo, 12);
let listed = repo.run_ok(&["issue", "list"]);
let shown: Vec<String> = listed
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| first_column(l).to_string())
.collect();
assert_eq!(shown.len(), 12, "expected 12 listed issues: {}", listed);
for id in &shown {
// Resolving is the assertion; run_ok panics on a non-zero exit, which
// is what an ambiguous or unknown prefix produces.
let detail = repo.run_ok(&["issue", "show", id]);
assert!(
detail.contains(id),
"showing '{}' should echo it back: {}",
id,
detail
);
}
}
/// The ids in `issue list` and `issue show` must agree, or the id someone
/// copies depends on which command they happened to run.
#[test]
fn list_and_show_print_the_same_width() {
let repo = TestRepo::new("Alice", "alice@example.com");
let ids = open_issues(&repo, 5);
let listed = repo.run_ok(&["issue", "list"]);
let listed_width = first_column(listed.lines().next().unwrap()).len();
let shown = repo.run_ok(&["issue", "show", &ids[0]]);
let header = shown.lines().next().unwrap();
let shown_id = header
.strip_prefix("Issue ")
.and_then(|rest| rest.split_whitespace().next())
.expect("show should start with an Issue header");
assert_eq!(shown_id.len(), listed_width, "header was: {}", header);
}
/// The abbreviation is computed over every issue of that kind, so `--all`
/// cannot print a different string for the same issue than the default view.
#[test]
fn closed_issues_do_not_change_the_width_of_open_ones() {
let repo = TestRepo::new("Alice", "alice@example.com");
let ids = open_issues(&repo, 6);
let before = repo.run_ok(&["issue", "list"]);
let before_id = first_column(before.lines().next().unwrap()).to_string();
for id in ids.iter().take(3) {
repo.run_ok(&["issue", "close", id]);
}
let after = repo.run_ok(&["issue", "list"]);
let after_ids: Vec<&str> = after
.lines()
.filter(|l| !l.trim().is_empty())
.map(first_column)
.collect();
assert!(
after_ids.iter().all(|id| id.len() == before_id.len()),
"closing issues changed the printed width: {}",
after
);
}
/// The other half of the policy: a prefix short enough to name two objects
/// must fail at the point of use. Twenty issues over sixteen possible first
/// characters guarantees a shared one.
#[test]
fn an_ambiguous_prefix_fails_loudly_instead_of_picking_one() {
let repo = TestRepo::new("Alice", "alice@example.com");
let ids = open_issues(&repo, 20);
let mut seen: std::collections::HashMap<char, usize> = std::collections::HashMap::new();
for id in &ids {
*seen.entry(id.chars().next().unwrap()).or_default() += 1;
}
let (shared, count) = seen
.iter()
.find(|(_, n)| **n > 1)
.expect("20 ids over 16 first characters must share one");
let err = repo.run_err(&["issue", "show", &shared.to_string()]);
assert!(
err.contains("ambiguous"),
"a prefix matching {} issues should be refused as ambiguous, got: {}",
count,
err
);
}
/// A prefix shorter than the displayed width still works when it happens to be
/// unique — accepting prefixes and displaying them are separate concerns, and
/// the accepting side stays as permissive as it was.
#[test]
fn a_unique_short_prefix_is_still_accepted() {
let repo = TestRepo::new("Alice", "alice@example.com");
let ids = open_issues(&repo, 1);
let short = &ids[0][..3];
let out = repo.run_ok(&["issue", "show", short]);
assert!(out.starts_with("Issue "), "got: {}", out);
}
// ---------------------------------------------------------------------------
// `collab.abbrev`, this tool's `core.abbrev`.
//
// These also serve as the proof that every display site actually goes through
// the abbreviator rather than a hard-coded 8: a repository small enough to
// test by hand can never produce a natural collision, so the width has to be
// forced to see the wiring at all.
// ---------------------------------------------------------------------------
fn set_abbrev(repo: &TestRepo, value: &str) {
let status = std::process::Command::new("git")
.args(["config", "collab.abbrev", value])
.current_dir(repo.dir.path())
.status()
.expect("failed to set config");
assert!(status.success());
}
#[test]
fn collab_abbrev_sets_the_width_of_every_list() {
let repo = TestRepo::new("Alice", "alice@example.com");
open_issues(&repo, 3);
set_abbrev(&repo, "14");
let listed = repo.run_ok(&["issue", "list"]);
for line in listed.lines().filter(|l| !l.trim().is_empty()) {
assert_eq!(
first_column(line).len(),
14,
"issue list ignored collab.abbrev: {}",
line
);
}
}
#[test]
fn collab_abbrev_sets_the_width_of_show_and_status() {
let repo = TestRepo::new("Alice", "alice@example.com");
let ids = open_issues(&repo, 2);
set_abbrev(&repo, "14");
let shown = repo.run_ok(&["issue", "show", &ids[0]]);
let header = shown.lines().next().unwrap();
let shown_id = header
.strip_prefix("Issue ")
.and_then(|r| r.split_whitespace().next())
.expect("show should start with an Issue header");
assert_eq!(shown_id.len(), 14, "show ignored collab.abbrev: {}", header);
let status = repo.run_ok(&["status"]);
let recent = status
.lines()
.find(|l| l.trim_start().starts_with("[issue]"))
.expect("status should list a recent issue");
let status_id = recent.split_whitespace().nth(1).unwrap();
assert_eq!(
status_id.len(),
14,
"status ignored collab.abbrev: {}",
recent
);
}
#[test]
fn collab_abbrev_applies_to_patches_too() {
let repo = TestRepo::new("Alice", "alice@example.com");
repo.git(&["checkout", "-b", "feature"]);
std::fs::write(repo.dir.path().join("a.txt"), "hello").unwrap();
repo.git(&["add", "a.txt"]);
repo.git(&["commit", "-m", "work"]);
repo.run_ok(&["patch", "create", "-t", "a patch", "--base", "main"]);
set_abbrev(&repo, "14");
let listed = repo.run_ok(&["patch", "list"]);
for line in listed.lines().filter(|l| !l.trim().is_empty()) {
assert_eq!(
first_column(line).len(),
14,
"patch list ignored collab.abbrev: {}",
line
);
}
}
/// `no` is git's spelling for "do not abbreviate".
#[test]
fn collab_abbrev_no_prints_whole_ids() {
let repo = TestRepo::new("Alice", "alice@example.com");
open_issues(&repo, 2);
set_abbrev(&repo, "no");
let listed = repo.run_ok(&["issue", "list"]);
for line in listed.lines().filter(|l| !l.trim().is_empty()) {
assert_eq!(first_column(line).len(), 40, "line: {}", line);
}
}
/// git clamps `core.abbrev` to a minimum of 4; below that the setting stops
/// being a display preference and starts producing ids nobody can use.
#[test]
fn collab_abbrev_is_clamped_to_a_usable_minimum() {
let repo = TestRepo::new("Alice", "alice@example.com");
open_issues(&repo, 2);
set_abbrev(&repo, "1");
let listed = repo.run_ok(&["issue", "list"]);
for line in listed.lines().filter(|l| !l.trim().is_empty()) {
assert_eq!(
first_column(line).len(),
4,
"collab.abbrev=1 should clamp to 4: {}",
line
);
}
}
/// A narrowed width is still never allowed to print an ambiguous id: the
/// per-id widening runs on top of whatever width is configured.
#[test]
fn a_narrow_configured_width_still_prints_resolvable_ids() {
let repo = TestRepo::new("Alice", "alice@example.com");
open_issues(&repo, 20);
set_abbrev(&repo, "4");
let listed = repo.run_ok(&["issue", "list"]);
for line in listed.lines().filter(|l| !l.trim().is_empty()) {
let id = first_column(line);
repo.run_ok(&["issue", "show", id]);
}
}
/// Nonsense in the config falls back to the automatic width rather than
/// breaking every command that prints an id.
#[test]
fn an_unparseable_collab_abbrev_falls_back_to_the_automatic_width() {
let repo = TestRepo::new("Alice", "alice@example.com");
open_issues(&repo, 2);
set_abbrev(&repo, "banana");
let listed = repo.run_ok(&["issue", "list"]);
for line in listed.lines().filter(|l| !l.trim().is_empty()) {
assert_eq!(first_column(line).len(), 8, "line: {}", line);
}
}
// ---------------------------------------------------------------------------
// The set the width is computed from
// ---------------------------------------------------------------------------
/// The abbreviation has to be computed over every object of a kind. If it were
/// computed over just the rows being listed, `issue list` and
/// `issue list --all` could print different strings for the same issue, and a
/// displayed id could name two objects the moment a different flag is used.
#[test]
fn the_id_set_covers_closed_and_archived_issues() {
use git2::Repository;
let repo = TestRepo::new("Alice", "alice@example.com");
let ids = open_issues(&repo, 3);
repo.run_ok(&["issue", "close", &ids[1]]);
let git = Repository::open(repo.dir.path()).unwrap();
let (_, full) = git_collab::state::resolve_issue_ref(&git, &ids[2]).unwrap();
git_collab::state::archive_issue_ref(&git, &full).unwrap();
let all = git_collab::state::all_issue_ids(&git).unwrap();
assert_eq!(all.len(), 3, "closed and archived ids must be included");
for id in &ids {
assert!(
all.iter().any(|found| found.starts_with(id)),
"'{}' missing from the id set: {:?}",
id,
all
);
}
}
#[test]
fn the_id_set_covers_archived_patches() {
use git2::Repository;
let repo = TestRepo::new("Alice", "alice@example.com");
repo.git(&["checkout", "-b", "feature"]);
std::fs::write(repo.dir.path().join("a.txt"), "hello").unwrap();
repo.git(&["add", "a.txt"]);
repo.git(&["commit", "-m", "work"]);
let out = repo.run_ok(&["patch", "create", "-t", "a patch", "--base", "main"]);
let short = out.trim().rsplit(' ').next().unwrap().to_string();
let git = Repository::open(repo.dir.path()).unwrap();
let (_, full) = git_collab::state::resolve_patch_ref(&git, &short).unwrap();
git_collab::state::archive_patch_ref(&git, &full).unwrap();
let all = git_collab::state::all_patch_ids(&git).unwrap();
assert!(
all.contains(&full),
"archived patch '{}' missing from {:?}",
full,
all
);
}
// ---------------------------------------------------------------------------
// The other two surfaces
//
// The web UI and the dashboard both render a *filtered* subset — open issues,
// merged patches, whatever the current view asks for. The policy is computed
// over every object of a kind, so a filtered view has to be abbreviated at a
// width it cannot derive from its own rows. These tests are written to fail
// against a surface that abbreviates over what it is showing.
// ---------------------------------------------------------------------------
/// Give `real_id` a twin that shares its first eight characters.
///
/// Eight is the floor width, so two ids only collide there in a repository far
/// too large to build in a test. Planting the collision is the only way to see
/// the widening at all, and an *archived* twin is the honest place to plant
/// it: archived objects are in the id set the policy is computed over and in
/// none of the views that list objects. A surface that abbreviates over the
/// rows it is showing prints eight characters here, and is wrong.
///
/// Returns the twin's id. The real id must now be printed nine characters
/// wide: identical through the floor width, distinct one character later.
fn plant_archived_issue_twin(git: &git2::Repository, real_id: &str) -> String {
let mut chars: Vec<char> = real_id.chars().collect();
chars[8] = if chars[8] == '0' { '1' } else { '0' };
let twin: String = chars.into_iter().collect();
let target = git
.refname_to_id(&format!("refs/collab/issues/{}", real_id))
.expect("the real issue ref should exist");
git.reference(
&format!("refs/collab/archive/issues/{}", twin),
target,
false,
"planted twin",
)
.unwrap();
twin
}
/// The full 40-character id behind whatever prefix a command printed.
fn full_issue_id(git: &git2::Repository, prefix: &str) -> String {
git_collab::state::resolve_issue_ref(git, prefix).unwrap().1
}
// ---------------------------------------------------------------------------
// web ui
// ---------------------------------------------------------------------------
fn set_server_abbrev(harness: &ServerHarness, value: &str) {
let repo_dir = harness
.repos_dir()
.join(format!("{}.git", harness.repo_name()));
common::git_cmd(&repo_dir, &["config", "collab.abbrev", value]);
}
/// The text of the `<a>` whose href ends in `href_tail`. The list templates
/// link the row's id cell to the object's own page, so this is exactly the
/// string the page offers a reader to copy.
fn anchor_text(body: &str, href_tail: &str) -> String {
let needle = format!("{}\">", href_tail);
let start = body
.find(&needle)
.unwrap_or_else(|| panic!("no link ending in '{}' in:\n{}", href_tail, body))
+ needle.len();
let end = body[start..]
.find("</a>")
.unwrap_or_else(|| panic!("unterminated link for '{}'", href_tail))
+ start;
body[start..end].to_string()
}
/// The test the naive fix fails. The issue list defaults to open issues only;
/// the twin that forces the widening is archived and appears on none of these
/// pages. Its absence must not narrow the id.
#[test]
fn a_filtered_issue_page_is_abbreviated_at_the_full_set_width() {
let harness = ServerHarness::new("abbrev-web-filtered");
harness.push_head();
let short = harness.work_repo().issue_open("A visible issue");
let git = git2::Repository::open(harness.work_repo().dir.path()).unwrap();
let real_id = full_issue_id(&git, &short);
let twin = plant_archived_issue_twin(&git, &real_id);
harness.push_collab_refs();
let page = harness.get_ok(&format!("/{}/issues?filter=open", harness.repo_name()));
let shown = anchor_text(&page.body, &format!("/issues/{}", real_id));
assert_eq!(
shown.len(),
9,
"the open-issue page abbreviated over its own rows instead of every \
issue: showed '{}' while an archived issue shares its first eight \
characters",
shown
);
assert!(real_id.starts_with(&shown), "showed '{}'", shown);
assert!(
!twin.starts_with(&shown),
"'{}' names two issues, so it is not a usable reference",
shown
);
}
/// The same issue must render as the same string whichever filter is applied,
/// or the id someone copies depends on which tab they happened to be on.
#[test]
fn every_issue_filter_prints_the_same_width() {
let harness = ServerHarness::new("abbrev-web-agree");
harness.push_head();
let short = harness.work_repo().issue_open("A visible issue");
let git = git2::Repository::open(harness.work_repo().dir.path()).unwrap();
let real_id = full_issue_id(&git, &short);
plant_archived_issue_twin(&git, &real_id);
harness.push_collab_refs();
let tail = format!("/issues/{}", real_id);
let open = harness.get_ok(&format!("/{}/issues?filter=open", harness.repo_name()));
let all = harness.get_ok(&format!("/{}/issues?filter=all", harness.repo_name()));
let overview = harness.get_ok(&format!("/{}", harness.repo_name()));
let from_open = anchor_text(&open.body, &tail);
assert_eq!(from_open, anchor_text(&all.body, &tail), "open vs all");
assert_eq!(
from_open,
anchor_text(&overview.body, &tail),
"issue list vs repo overview"
);
}
#[test]
fn collab_abbrev_sets_the_width_of_the_web_issue_and_patch_lists() {
let harness = ServerHarness::new("abbrev-web-config");
harness.push_head();
let issue_short = harness.work_repo().issue_open("Configured issue");
let patch_short = harness.work_repo().patch_create("Configured patch");
harness.push_collab_refs();
set_server_abbrev(&harness, "14");
let git = git2::Repository::open(harness.work_repo().dir.path()).unwrap();
let issue_id = full_issue_id(&git, &issue_short);
let patch_id = git_collab::state::resolve_patch_ref(&git, &patch_short)
.unwrap()
.1;
let issues = harness.get_ok(&format!("/{}/issues", harness.repo_name()));
assert_eq!(
anchor_text(&issues.body, &format!("/issues/{}", issue_id)).len(),
14,
"the issue list ignored collab.abbrev"
);
let patches = harness.get_ok(&format!("/{}/patches", harness.repo_name()));
assert_eq!(
anchor_text(&patches.body, &format!("/patches/{}", patch_id)).len(),
14,
"the patch list ignored collab.abbrev"
);
let overview = harness.get_ok(&format!("/{}", harness.repo_name()));
assert_eq!(
anchor_text(&overview.body, &format!("/issues/{}", issue_id)).len(),
14,
"the repo overview ignored collab.abbrev"
);
assert_eq!(
anchor_text(&overview.body, &format!("/patches/{}", patch_id)).len(),
14,
"the repo overview ignored collab.abbrev"
);
}
/// `full` is git's spelling for "do not abbreviate", and the web UI has the
/// room to honour it.
#[test]
fn collab_abbrev_full_prints_whole_ids_in_the_web_ui() {
let harness = ServerHarness::new("abbrev-web-full");
harness.push_head();
let issue_short = harness.work_repo().issue_open("Whole id issue");
harness.push_collab_refs();
set_server_abbrev(&harness, "full");
let git = git2::Repository::open(harness.work_repo().dir.path()).unwrap();
let issue_id = full_issue_id(&git, &issue_short);
let issues = harness.get_ok(&format!("/{}/issues", harness.repo_name()));
assert_eq!(
anchor_text(&issues.body, &format!("/issues/{}", issue_id)),
issue_id,
"collab.abbrev=full should print the whole id"
);
}
// ---------------------------------------------------------------------------
// Reading is not writing
//
// Both surfaces now run an extra query — every id of a kind — on every render.
// It has to stay a query. A page view or a dashboard launch that appended an
// event or moved a ref would be writing to a shared, replicated history on
// behalf of someone who only looked at it.
// ---------------------------------------------------------------------------
/// Every collab ref and what it points at, as a sorted list.
fn collab_refs_snapshot(git_dir: &std::path::Path) -> Vec<String> {
let out = std::process::Command::new("git")
.args([
"for-each-ref",
"--format=%(refname) %(objectname)",
"refs/collab/",
])
.current_dir(git_dir)
.output()
.expect("for-each-ref failed");
let mut refs: Vec<String> = String::from_utf8_lossy(&out.stdout)
.lines()
.map(|l| l.to_string())
.collect();
refs.sort();
refs
}
#[test]
fn rendering_web_pages_does_not_touch_the_collab_refs() {
let harness = ServerHarness::new("abbrev-web-readonly");
harness.push_head();
harness.work_repo().issue_open("An issue to look at");
harness.work_repo().patch_create("A patch to look at");
harness.push_collab_refs();
let served = harness
.repos_dir()
.join(format!("{}.git", harness.repo_name()));
let before = collab_refs_snapshot(&served);
assert!(!before.is_empty(), "the test needs some refs to protect");
let name = harness.repo_name();
for path in [
format!("/{}", name),
format!("/{}/issues", name),
format!("/{}/issues?filter=all", name),
format!("/{}/patches", name),
format!("/{}/patches?filter=all", name),
] {
harness.get_ok(&path);
}
assert_eq!(
before,
collab_refs_snapshot(&served),
"rendering a page changed the collab refs"
);
}
#[test]
fn launching_the_dashboard_does_not_touch_the_collab_refs() {
let repo = TestRepo::new("Alice", "alice@example.com");
repo.issue_open("An issue to look at");
repo.patch_create("A patch to look at");
let before = collab_refs_snapshot(repo.dir.path());
assert!(!before.is_empty(), "the test needs some refs to protect");
// `a` cycles the status filter and `P` swaps to patches, so this walks
// the views that re-derive a width, not just the one on startup.
let output = repo.run_dashboard_smoke_sized("aPaq", 120, 24);
assert!(output.status.success(), "dashboard should exit cleanly");
assert_eq!(
before,
collab_refs_snapshot(repo.dir.path()),
"opening the dashboard changed the collab refs"
);
}
// ---------------------------------------------------------------------------
// patch checkout
//
// The branch `patch checkout` creates is named after the patch, so the branch
// name is a displayed id like any other — and the one displayed id that
// outlives the command by definition, since the branch stays behind.
// ---------------------------------------------------------------------------
/// The patch equivalent of `plant_archived_issue_twin`.
fn plant_archived_patch_twin(git: &git2::Repository, real_id: &str) -> String {
let mut chars: Vec<char> = real_id.chars().collect();
chars[8] = if chars[8] == '0' { '1' } else { '0' };
let twin: String = chars.into_iter().collect();
let target = git
.refname_to_id(&format!("refs/collab/patches/{}/events", real_id))
.expect("the real patch ref should exist");
git.reference(
&format!("refs/collab/archive/patches/{}/events", twin),
target,
false,
"planted twin",
)
.unwrap();
twin
}
/// Two patches sharing eight characters must not want the same branch name.
///
/// The suffix loop in `checkout` would paper over it — `collab/<id>-1` — but
/// that name reads as a second checkout of one patch rather than a checkout of
/// a different one, and the reviewer has no way to tell which patch they are
/// looking at from the branch they are on.
#[test]
fn patch_checkout_names_the_branch_at_the_full_set_width() {
let repo = TestRepo::new("Alice", "alice@example.com");
let short = repo.patch_create("A patch to check out");
let git = git2::Repository::open(repo.dir.path()).unwrap();
let real_id = git_collab::state::resolve_patch_ref(&git, &short)
.unwrap()
.1;
let twin = plant_archived_patch_twin(&git, &real_id);
let out = repo.run_ok(&["patch", "checkout", &real_id]);
let branch = repo.git(&["rev-parse", "--abbrev-ref", "HEAD"]);
let branch = branch.trim();
assert_eq!(
branch,
format!("collab/{}", &real_id[..9]),
"the branch name was cut to a prefix that also names patch {}",
&twin[..9]
);
assert!(
out.contains(&real_id[..9]),
"the message should name the patch at the same width: {}",
out
);
}
#[test]
fn collab_abbrev_sets_the_width_of_the_patch_checkout_branch() {
let repo = TestRepo::new("Alice", "alice@example.com");
let short = repo.patch_create("A configured patch");
let git = git2::Repository::open(repo.dir.path()).unwrap();
let real_id = git_collab::state::resolve_patch_ref(&git, &short)
.unwrap()
.1;
set_abbrev(&repo, "14");
repo.run_ok(&["patch", "checkout", &real_id]);
let branch = repo.git(&["rev-parse", "--abbrev-ref", "HEAD"]);
assert_eq!(branch.trim(), format!("collab/{}", &real_id[..14]));
}
// ---------------------------------------------------------------------------
// dashboard
//
// The TUI is driven through a pty, so what these assert on is the characters
// that actually reached the screen.
// ---------------------------------------------------------------------------
/// The printable text of a terminal session, with the escape sequences that
/// carried the colours removed.
fn strip_ansi(raw: &str) -> String {
let mut out = String::new();
let mut chars = raw.chars().peekable();
while let Some(c) = chars.next() {
if c != '\u{1b}' {
out.push(c);
continue;
}
// CSI sequences run until a byte in @-~; anything else is a two-byte
// escape. Neither ever contains printable payload the dashboard means
// for a reader, so both are dropped whole.
if chars.peek() == Some(&'[') {
chars.next();
for c2 in chars.by_ref() {
if ('@'..='~').contains(&c2) {
break;
}
}
} else {
chars.next();
}
}
out
}
fn dashboard_screen(repo: &TestRepo, cols: u16) -> String {
let output = repo.run_dashboard_smoke_sized("q", cols, 24);
assert!(
output.status.success(),
"dashboard did not exit cleanly: {}",
String::from_utf8_lossy(&output.stderr)
);
strip_ansi(&String::from_utf8_lossy(&output.stdout))
}
/// Open one issue and give it an archived twin sharing its first eight
/// characters. Returns the issue's full id.
fn issue_with_archived_twin(repo: &TestRepo, title: &str) -> (String, String) {
let short = repo.issue_open(title);
let git = git2::Repository::open(repo.dir.path()).unwrap();
let real_id = full_issue_id(&git, &short);
let twin = plant_archived_issue_twin(&git, &real_id);
(real_id, twin)
}
/// The dashboard's counterpart to the web test above. Its issue list defaults
/// to open issues and never shows archived ones at all, yet the archived twin
/// still has to widen what it prints.
#[test]
fn the_dashboard_issue_list_is_abbreviated_at_the_full_set_width() {
let repo = TestRepo::new("Alice", "alice@example.com");
let (real_id, twin) = issue_with_archived_twin(&repo, "Dashboard issue");
let screen = dashboard_screen(&repo, 120);
assert!(
screen.contains(&format!("{} ", &real_id[..9])),
"expected the issue id widened to nine characters ('{}') on screen:\n{}",
&real_id[..9],
screen
);
assert!(
!screen.contains(&format!("{} ", &real_id[..8])),
"the dashboard printed '{}', which also names the archived issue {}",
&real_id[..8],
&twin[..9]
);
}
#[test]
fn collab_abbrev_sets_the_width_of_the_dashboard_lists() {
let repo = TestRepo::new("Alice", "alice@example.com");
let short = repo.issue_open("Configured issue");
let git = git2::Repository::open(repo.dir.path()).unwrap();
let issue_id = full_issue_id(&git, &short);
set_abbrev(&repo, "14");
let screen = dashboard_screen(&repo, 120);
assert!(
screen.contains(&format!("{} ", &issue_id[..14])),
"the dashboard ignored collab.abbrev=14:\n{}",
screen
);
}
#[test]
fn collab_abbrev_full_prints_whole_ids_in_the_dashboard() {
let repo = TestRepo::new("Alice", "alice@example.com");
let short = repo.issue_open("Whole id issue");
let git = git2::Repository::open(repo.dir.path()).unwrap();
let issue_id = full_issue_id(&git, &short);
set_abbrev(&repo, "full");
// Wide enough that a 40-character id is not merely clipped by the pane.
let screen = dashboard_screen(&repo, 200);
assert!(
screen.contains(&issue_id),
"collab.abbrev=full should print the whole id '{}':\n{}",
issue_id,
screen
);
}