use assert_cmd::Command;
use predicates::prelude::*;
use std::path::Path;
use tempfile::TempDir;
fn tix() -> Command {
Command::cargo_bin("tix").unwrap()
}
fn repo() -> TempDir {
let tmp = TempDir::new().unwrap();
tix().current_dir(tmp.path()).arg("init").assert().success();
tmp
}
fn create(dir: &Path, title: &str, extra: &[&str]) -> String {
let out = tix()
.current_dir(dir)
.args(["create", "--title", title])
.args(extra)
.assert()
.success()
.get_output()
.stdout
.clone();
String::from_utf8(out).unwrap()
}
fn stdout(dir: &Path, args: &[&str]) -> String {
let out = tix().current_dir(dir).args(args).output().unwrap();
assert!(
out.status.success(),
"{args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
fn row_numbers(text: &str) -> Vec<u32> {
text.lines()
.skip(1)
.filter_map(|line| line.split_whitespace().next())
.filter_map(|first| first.parse().ok())
.collect()
}
#[test]
fn ready_orders_startable_work_and_leaves_out_blocked_and_started_issues() {
let tmp = repo();
let dir = tmp.path();
create(dir, "Alpha", &["--priority", "high"]);
create(dir, "Bravo", &["--priority", "high"]);
create(dir, "Charlie", &["--priority", "medium"]);
create(
dir,
"Delta",
&["--priority", "high", "--status", "in progress"],
);
create(dir, "Echo", &["--priority", "high"]);
tix()
.current_dir(dir)
.args(["link", "1", "5", "--type", "blocks"])
.assert()
.success();
let ready = stdout(dir, &["ready"]);
assert_eq!(
row_numbers(&ready),
vec![1, 2, 3],
"high (#1, #2) before medium (#3), oldest first; #4 is under way and #5 is \
blocked:\n{ready}"
);
assert_eq!(
stdout(dir, &["ready", "--limit", "1"]).lines().count(),
2,
"the limit is applied after the ordering, so `--limit 1` is one row"
);
assert_eq!(
row_numbers(&stdout(dir, &["ready", "--limit", "1"])),
vec![1]
);
let header = ready.lines().next().unwrap();
assert_eq!(
header.split_whitespace().collect::<Vec<_>>(),
["NUM", "ID", "PRIORITY", "TITLE"]
);
}
#[test]
fn ready_returns_an_issue_once_its_blocker_is_complete() {
let tmp = repo();
let dir = tmp.path();
create(dir, "Blocker", &[]);
create(dir, "Blocked", &[]);
tix()
.current_dir(dir)
.args(["link", "1", "2", "--type", "blocks"])
.assert()
.success();
assert_eq!(row_numbers(&stdout(dir, &["ready"])), vec![1]);
tix()
.current_dir(dir)
.args(["move", "1", "done", "--commit", "abc1234"])
.assert()
.success();
assert_eq!(
row_numbers(&stdout(dir, &["ready"])),
vec![2],
"a complete blocker no longer holds anything up"
);
}
#[test]
fn the_default_listing_is_scoped_and_all_restores_the_board() {
let tmp = repo();
let dir = tmp.path();
create(dir, "Accepted work", &["--status", "todo"]);
create(dir, "Work under way", &["--status", "in progress"]);
create(dir, "Awaiting triage", &[]);
let default = stdout(dir, &["list"]);
assert_eq!(
row_numbers(&default),
vec![1],
"only the entry column:\n{default}"
);
let all = stdout(dir, &["list", "--all"]);
assert_eq!(row_numbers(&all), vec![1, 2, 3], "--all is the whole board");
let started = stdout(dir, &["list", "--status", "in progress"]);
assert_eq!(
row_numbers(&started),
vec![2],
"an explicit --status replaces the scope's, it does not compose with it"
);
let inbox = stdout(dir, &["list", "--status", "triage", "--columns", "num,id"]);
let id = inbox
.lines()
.nth(1)
.and_then(|line| line.split_whitespace().nth(1))
.unwrap_or_default()
.to_string();
assert_eq!(id.len(), 8, "the canonical id, not the number: {inbox}");
assert!(id.chars().all(|c| c.is_ascii_alphanumeric()), "{inbox}");
}
#[test]
fn the_structured_formats_carry_the_displayed_columns() {
let tmp = repo();
let dir = tmp.path();
create(
dir,
"Formatted",
&["--priority", "high", "--status", "todo"],
);
let toml = stdout(dir, &["ready", "--format", "toml"]);
assert!(toml.contains("[[issues]]"), "{toml}");
assert!(toml.contains(r#"title = "Formatted""#), "{toml}");
assert!(toml.contains(r#"priority = "high""#), "{toml}");
let yaml = stdout(dir, &["ready", "--format", "yml"]);
assert!(yaml.starts_with("issues:\n"), "{yaml}");
assert!(yaml.contains(" title: Formatted"), "{yaml}");
let xml = stdout(dir, &["ready", "--format", "xml"]);
assert!(
xml.contains("<issues>") && xml.contains("</issues>"),
"{xml}"
);
assert!(xml.contains("<title>Formatted</title>"), "{xml}");
let chosen = stdout(
dir,
&["ready", "--columns", "num,status", "--format", "toml"],
);
assert!(chosen.contains("num = "), "{chosen}");
assert!(chosen.contains("status = "), "{chosen}");
assert!(!chosen.contains("title = "), "{chosen}");
tix()
.current_dir(dir)
.args(["ready", "--format", "json"])
.assert()
.failure()
.stderr(predicate::str::contains("unknown list format `json`"));
}
#[test]
fn a_piped_listing_is_not_cropped() {
let tmp = repo();
let dir = tmp.path();
let title = "A title that is far longer than any reasonable terminal width would \
ever allow in one line of a table";
create(dir, title, &["--status", "todo"]);
let listed = stdout(dir, &["list"]);
assert!(
listed.contains(title),
"no terminal, no crop — the width is the table's own:\n{listed}"
);
}