use headwater_cli::paint::{WIDEST, WIDTH};
use std::path::{Path, PathBuf};
use std::process::Command as Process;
fn repository() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.canonicalize()
.expect("the repository root resolves")
}
struct Ran {
code: Option<i32>,
out: Vec<u8>,
err: Vec<u8>,
}
fn ran(arguments: &[&str], columns: Option<&str>) -> Ran {
let at = std::env::temp_dir().join(format!("headwater-width-{}", std::process::id()));
std::fs::create_dir_all(&at).expect("the directory is there");
let mut process = Process::new(env!("CARGO_BIN_EXE_headwater"));
process
.args(arguments)
.current_dir(&at)
.env_remove("COLUMNS");
if let Some(value) = columns {
process.env("COLUMNS", value);
}
let output = process.output().expect("the binary runs");
Ran {
code: output.status.code(),
out: output.stdout,
err: output.stderr,
}
}
fn command_lines() -> Vec<Vec<String>> {
fn walk(command: &clap::Command, at: Vec<String>, into: &mut Vec<Vec<String>>) {
into.push(at.clone());
for inner in command.get_subcommands() {
let mut next = at.clone();
next.push(inner.get_name().to_string());
walk(inner, next, into);
}
}
let mut command = headwater_cli::command();
command.build();
let mut lines = Vec::new();
walk(&command, Vec::new(), &mut lines);
lines
}
fn surface(columns: Option<&str>, extra: &[&str]) -> Vec<(String, String)> {
command_lines()
.into_iter()
.map(|line| {
let mut arguments: Vec<&str> = line.iter().map(String::as_str).collect();
arguments.push("--help");
arguments.extend_from_slice(extra);
let ran = ran(&arguments, columns);
let typed = format!("headwater {}", arguments.join(" "));
assert_eq!(ran.code, Some(0), "{typed} exits 0");
assert!(
ran.err.is_empty(),
"{typed} writes nothing to standard error"
);
(typed, String::from_utf8(ran.out).expect("the help is text"))
})
.collect()
}
fn widest(surface: &[(String, String)]) -> (usize, String, String) {
surface
.iter()
.flat_map(|(typed, out)| {
out.lines()
.map(move |line| (line.chars().count(), typed.clone(), line.to_string()))
})
.max_by_key(|(width, _, _)| *width)
.expect("the surface has a line")
}
#[test]
fn no_line_of_the_help_surface_is_wider_than_eighty_columns() {
let surface = surface(None, &[]);
let mut lines = 0;
let mut over: Vec<String> = Vec::new();
for (typed, out) in &surface {
for (at, line) in out.lines().enumerate() {
lines += 1;
let width = line.chars().count();
if width > WIDTH {
over.push(format!("{typed}, line {}, {width} columns", at + 1));
}
}
}
let (widest, where_, text) = widest(&surface);
assert!(
over.is_empty(),
"{} of {lines} lines over {WIDTH} columns across {} command lines. \
The widest is {widest} columns, in `{where_}`:\n{text}\n\nEvery one:\n{}",
over.len(),
surface.len(),
over.join("\n")
);
assert!(widest <= WIDTH, "the widest line is {widest} columns");
}
#[test]
fn a_run_that_states_columns_and_one_that_does_not_write_the_same_bytes() {
for line in [
vec!["--help"],
vec!["check", "--help"],
vec!["help", "check"],
] {
let bare = ran(&line, None);
let written = String::from_utf8(bare.out.clone()).expect("the help is text");
for absurd in ["1", "40", "500", "100000", "not a number"] {
let stated = ran(&line, Some(absurd));
assert_eq!(
written,
String::from_utf8(stated.out).expect("the help is text"),
"`headwater {}` under COLUMNS={absurd} writes what it writes with none",
line.join(" ")
);
assert_eq!(bare.code, stated.code);
assert!(stated.err.is_empty());
}
}
}
#[test]
fn the_width_a_caller_asks_for_is_read_and_held_to_the_band() {
let line = ["check", "--help"];
let text = |ran: Ran| String::from_utf8(ran.out).expect("the help is text");
let ordinary = text(ran(&line, None));
let wide = |columns: &str| {
let mut arguments = line.to_vec();
arguments.push("--wide");
text(ran(&arguments, Some(columns)))
};
let narrow = wide("40");
assert_eq!(narrow, ordinary, "a width under {WIDTH} is {WIDTH}");
assert_eq!(wide("80"), ordinary);
let widest_asked = wide("500");
assert_eq!(
widest_asked,
wide("120"),
"a width over {WIDEST} is {WIDEST}"
);
assert_ne!(widest_asked, ordinary, "{WIDEST} is not {WIDTH}");
let between = wide("100");
assert_ne!(between, ordinary);
assert_ne!(between, widest_asked);
for (asked, out) in [(WIDTH, &narrow), (100, &between), (WIDEST, &widest_asked)] {
for line in out.lines() {
assert!(
line.chars().count() <= asked,
"at {asked} columns this line is {}: {line}",
line.chars().count()
);
}
}
}
#[test]
fn the_widest_a_caller_may_ask_for_lays_the_whole_surface_out_inside_it() {
let surface = surface(Some("500"), &["--wide"]);
let (widest, where_, text) = widest(&surface);
assert!(
widest <= WIDEST,
"`{where_}` has a line of {widest} columns at the widest band:\n{text}"
);
assert!(widest > WIDTH, "the widest band widened nothing: {widest}");
}
#[test]
fn a_width_asked_for_a_run_that_lays_nothing_out_is_refused() {
for (line, says) in [
(
vec!["check", "--wide", "--format", "json"],
"`--format json` writes an artifact",
),
(
vec!["check", "--wide", "--format", "sarif"],
"`--format sarif` writes an artifact",
),
(
vec!["check", "--wide", "--format", "markdown"],
"`--format markdown` writes an artifact",
),
(
vec!["capture", "--wide", "--format", "json"],
"`--format json` writes an artifact",
),
(
vec!["export", "--wide", "--format", "json"],
"`--format json` writes an artifact",
),
(
vec!["capture", "--wide", "--format", "text"],
"lays nothing out",
),
(vec!["taxonomy", "audit", "--wide"], "lays nothing out"),
(
vec!["check", "--wide", "--json"],
"`--json` writes an artifact",
),
(
vec!["capture", "--wide", "--json"],
"`--json` writes an artifact",
),
] {
let typed = line.join(" ");
let ran = ran(&line, None);
assert_eq!(ran.code, Some(1), "`headwater {typed}` is refused with 1");
let said = String::from_utf8_lossy(&ran.err).into_owned();
assert!(
said.contains("--wide"),
"the refusal names the flag: {said}"
);
assert!(
said.contains("how wide the help"),
"the refusal says what the flag does: {said}"
);
assert!(
said.contains(says),
"`headwater {typed}` should say `{says}`, and said: {said}"
);
assert!(ran.out.is_empty(), "a refusal writes no artifact");
}
}
#[test]
fn both_spellings_of_a_machine_format_answer_a_width_the_same_way() {
let root = repository();
let root = root.to_str().expect("the path is text").to_string();
let one = ran(&["check", "--wide", "--json", "--root", &root], None);
let two = ran(
&["check", "--wide", "--format", "json", "--root", &root],
None,
);
assert_eq!(one.code, Some(1), "`--json` beside `--wide` is refused");
assert_eq!(one.code, two.code, "the two spellings exit the same way");
assert!(
one.out.is_empty() && two.out.is_empty(),
"neither wrote one"
);
let plain_one = ran(&["check", "--json", "--root", &root], None);
let plain_two = ran(&["check", "--format", "json", "--root", &root], None);
assert_eq!(plain_one.code, Some(0));
assert_eq!(
plain_one.out, plain_two.out,
"the two spellings write the same artifact"
);
}
#[test]
fn a_width_asked_for_the_report_that_is_laid_out_is_answered() {
let root = repository();
let root = root.to_str().expect("the path is text").to_string();
for line in [
vec!["check", "--wide", "--root", &root],
vec!["check", "--wide", "--format", "text", "--root", &root],
] {
let typed = line.join(" ");
let ran = ran(&line, Some("120"));
assert_eq!(ran.code, Some(0), "`headwater {typed}` is answered");
assert!(!ran.out.is_empty(), "`headwater {typed}` wrote a report");
let said = String::from_utf8_lossy(&ran.err).into_owned();
assert!(
!said.contains("--wide"),
"`headwater {typed}` says nothing about the flag: {said}"
);
}
}
#[test]
fn a_width_asked_for_a_run_that_prints_help_is_answered() {
for line in [
vec!["--wide", "--help"],
vec!["check", "--wide", "--help"],
vec!["check", "--wide", "--format", "text", "--help"],
vec!["help", "check", "--wide"],
vec!["--wide", "help", "check"],
vec!["help", "--wide"],
] {
let typed = line.join(" ");
let ran = ran(&line, Some("120"));
assert_eq!(ran.code, Some(0), "`headwater {typed}` is answered");
assert!(!ran.out.is_empty(), "`headwater {typed}` printed help");
assert!(
ran.err.is_empty(),
"`headwater {typed}` says nothing on standard error"
);
}
}
#[test]
fn no_escape_byte_reaches_a_caller_under_any_of_the_four_conditions() {
struct Case(
&'static str,
&'static [(&'static str, &'static str)],
&'static [&'static str],
);
let cases = [
Case("plain", &[], &["--help"]),
Case("NO_COLOR=1", &[("NO_COLOR", "1")], &["--help"]),
Case("NO_COLOR=", &[("NO_COLOR", "")], &["--help"]),
Case("NO_COLOR=0", &[("NO_COLOR", "0")], &["--help"]),
Case("TERM=dumb", &[("TERM", "dumb")], &["--help"]),
Case("--no-color", &[], &["--no-color", "--help"]),
Case("--no-color deep", &[], &["check", "--no-color", "--help"]),
Case("CLICOLOR_FORCE", &[("CLICOLOR_FORCE", "1")], &["--help"]),
Case("verb page", &[], &["check", "--help"]),
Case("second-word page", &[], &["taxonomy", "--help"]),
Case("help verb", &[], &["help", "check"]),
Case("help second word", &[], &["help", "taxonomy", "diff"]),
Case(
"CLICOLOR_FORCE on a verb page",
&[("CLICOLOR_FORCE", "1")],
&["check", "--help"],
),
Case(
"CLICOLOR_FORCE on help verb",
&[("CLICOLOR_FORCE", "1")],
&["help", "check"],
),
Case(
"CLICOLOR_FORCE on a completion script",
&[("CLICOLOR_FORCE", "1")],
&["completions", "bash"],
),
Case(
"NO_COLOR on a verb page",
&[("NO_COLOR", "1")],
&["check", "--help"],
),
Case(
"--no-color on help verb",
&[],
&["--no-color", "help", "check"],
),
];
for Case(label, environment, arguments) in cases {
let at = std::env::temp_dir().join(format!("headwater-color-{}", std::process::id()));
std::fs::create_dir_all(&at).expect("the directory is there");
let mut process = Process::new(env!("CARGO_BIN_EXE_headwater"));
process
.args(arguments)
.current_dir(&at)
.env_remove("COLUMNS");
for (name, value) in environment {
process.env(name, value);
}
let output = process.output().expect("the binary runs");
assert_eq!(output.status.code(), Some(0), "{label} exits 0");
assert!(
!contains_escape(&output.stdout),
"{label} writes an escape byte to standard output"
);
assert!(
!contains_escape(&output.stderr),
"{label} writes an escape byte to standard error"
);
}
}
#[test]
fn no_escape_byte_reaches_a_machine_format() {
let root = repository();
let root = root.to_str().expect("the path is text").to_string();
for format in ["json", "sarif", "markdown"] {
let ran = ran(&["check", "--format", format, "--root", &root], None);
assert_eq!(ran.code, Some(0), "`--format {format}` ran");
assert!(!ran.out.is_empty(), "`--format {format}` wrote a report");
assert!(
!contains_escape(&ran.out),
"`--format {format}` writes an escape byte to standard output"
);
assert!(
!contains_escape(&ran.err),
"`--format {format}` writes an escape byte to standard error"
);
}
}
#[test]
fn no_escape_byte_reaches_a_file_this_binary_writes() {
let root = repository();
let root = root.to_str().expect("the path is text").to_string();
let at = std::env::temp_dir().join(format!("headwater-artifacts-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("the directory is there");
for name in ["read-set", "register"] {
let path = at.join(format!("{name}.json"));
let written = path.to_str().expect("the path is text").to_string();
let ran = ran(
&["check", "--root", &root, &format!("--{name}"), &written],
None,
);
assert_eq!(ran.code, Some(0), "`--{name}` ran");
let bytes = std::fs::read(&path).unwrap_or_else(|_| panic!("`--{name}` wrote {written}"));
assert!(!bytes.is_empty(), "`--{name}` wrote something");
assert!(
!contains_escape(&bytes),
"the file `--{name}` wrote carries an escape byte"
);
}
}
#[test]
fn no_escape_byte_reaches_a_refusal_that_is_not_fail_or_the_bare_invocation() {
let at = std::env::temp_dir().join(format!("headwater-refusal-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("the directory is there");
let output = Process::new(env!("CARGO_BIN_EXE_headwater"))
.arg("check")
.current_dir(&at)
.env_remove("COLUMNS")
.output()
.expect("the binary runs");
assert_eq!(output.status.code(), Some(1), "no lock, so the run refuses");
assert!(
!output.stdout.is_empty() || !output.stderr.is_empty(),
"the refusal wrote something"
);
assert!(
!contains_escape(&output.stdout),
"the refusal writes an escape byte to standard output"
);
assert!(
!contains_escape(&output.stderr),
"the refusal writes an escape byte to standard error"
);
let text = String::from_utf8(output.stderr).expect("the refusal is text");
assert!(
text.contains("taxonomy.lock"),
"the refusal names the lock this run looked for:\n{text}"
);
}
#[test]
fn stripping_the_painted_help_gives_the_plain_help_byte_for_byte() {
use headwater_cli::paint::ColorMode;
let mut painted = headwater_cli::command_in(WIDTH, ColorMode::Ansi);
let mut plain = headwater_cli::command_in(WIDTH, ColorMode::Plain);
painted.build();
plain.build();
let lines = command_lines();
assert!(!lines.is_empty(), "the tree has pages to render");
let mut colored = 0usize;
for words in &lines {
let typed = format!("headwater {}", words.join(" "));
let with_color = help_at(&mut painted, words);
let without = help_at(&mut plain, words);
assert!(
!contains_escape(without.as_bytes()),
"`{typed} --help` renders an escape byte in ColorMode::Plain"
);
assert_eq!(
strip_sgr(&with_color),
without,
"`{typed} --help` moves a byte when it is painted"
);
if contains_escape(with_color.as_bytes()) {
colored += 1;
}
}
assert_eq!(
colored,
lines.len(),
"every page of the help is painted in ColorMode::Ansi, and {} of {} were",
colored,
lines.len()
);
}
fn help_at(command: &mut clap::Command, words: &[String]) -> String {
let mut cursor = command;
for word in words {
cursor = cursor
.find_subcommand_mut(word.as_str())
.unwrap_or_else(|| panic!("the tree carries `{word}`"));
}
cursor.render_help().ansi().to_string()
}
fn strip_sgr(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find("\x1b[") {
out.push_str(&rest[..at]);
let tail = &rest[at + 2..];
match tail.find('m') {
Some(end) => rest = &tail[end + 1..],
None => {
out.push_str(&rest[at..]);
return out;
}
}
}
out.push_str(rest);
out
}
fn contains_escape(bytes: &[u8]) -> bool {
bytes.windows(2).any(|pair| pair == [0x1b, b'['])
}
fn flat(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[test]
fn the_root_screen_alone_carries_the_masthead() {
let version = String::from_utf8(ran(&["--version"], None).out).expect("the version is text");
let version = version.trim();
let root = String::from_utf8(ran(&["--help"], None).out).expect("the help is text");
let lines: Vec<&str> = root.lines().collect();
assert_eq!(
lines[0],
format!("headwater {version} — a documentation corpus, governed and checked like code"),
"the first line names the binary and the version, not {:?}",
lines[0]
);
assert!(
!lines[1].is_empty() && lines[1].chars().all(|c| c == '─'),
"the second line is a rule of box-drawing dashes, not {:?}",
lines[1]
);
let usage_at = lines
.iter()
.position(|line| line.starts_with("Usage:"))
.expect("the root screen has a usage line");
assert!(usage_at > 1, "the rule sits above Usage:");
let help = String::from_utf8(ran(&["help"], None).out).expect("the help is text");
assert_eq!(
root, help,
"`headwater help` and `headwater --help` still agree"
);
let verb = String::from_utf8(ran(&["check", "--help"], None).out).expect("the help is text");
assert!(
!verb
.lines()
.any(|line| !line.is_empty() && line.chars().all(|c| c == '─')),
"a verb's own page carries no rule, and so no masthead"
);
let version_run = ran(&["--version"], None);
let version_out = String::from_utf8(version_run.out).expect("the version is text");
assert_eq!(
version_out.lines().count(),
1,
"--version still writes exactly one line"
);
let bare = ran(&[], None);
assert_eq!(bare.code, Some(1), "bare invocation still refuses");
let bare_err = String::from_utf8(bare.err).expect("the refusal is text");
assert!(
bare_err.contains("no verb"),
"the refusal keeps its wording, not {bare_err:?}"
);
assert!(
!bare_err
.lines()
.any(|line| !line.is_empty() && line.chars().all(|c| c == '─')),
"the refusal carries no masthead"
);
}
#[test]
fn no_banner_and_its_environment_variable_suppress_the_masthead_and_both_are_accepted_everywhere() {
struct Case(
&'static str,
&'static [(&'static str, &'static str)],
&'static [&'static str],
);
let cases = [
Case("--no-banner", &[], &["--no-banner", "--help"]),
Case(
"HEADWATER_NO_BANNER=1",
&[("HEADWATER_NO_BANNER", "1")],
&["--help"],
),
];
for Case(label, environment, arguments) in cases {
let at = std::env::temp_dir().join(format!("headwater-banner-{}", std::process::id()));
std::fs::create_dir_all(&at).expect("the directory is there");
let mut process = Process::new(env!("CARGO_BIN_EXE_headwater"));
process
.args(arguments)
.current_dir(&at)
.env_remove("COLUMNS");
for (name, value) in environment {
process.env(name, value);
}
let output = process.output().expect("the binary runs");
assert_eq!(output.status.code(), Some(0), "{label} exits 0");
let out = String::from_utf8(output.stdout).expect("the help is text");
assert!(
!out.lines()
.any(|line| !line.is_empty() && line.chars().all(|c| c == '─')),
"{label} suppresses the masthead's rule"
);
assert_eq!(
out.lines().next(),
Some("headwater — a documentation corpus, governed and checked like code"),
"{label} reverts the first line to today's plain name line"
);
}
let deep = ran(&["check", "--no-banner", "--help"], None);
assert_eq!(deep.code, Some(0), "--no-banner is accepted on a verb page");
assert!(
deep.err.is_empty(),
"and it is silent there, same as --no-color"
);
}
#[test]
fn no_color_s_own_text_states_the_new_behavior_and_no_banner_has_an_entry() {
let root = flat(&String::from_utf8(ran(&["--help"], None).out).expect("the help is text"));
assert!(
!root.contains("no run of this binary emits color"),
"the root screen's --no-color summary no longer claims this binary has no color"
);
assert!(
root.contains("suppress the masthead"),
"--no-banner has a summary line on the root screen, not just in:\n{root}"
);
let verb =
flat(&String::from_utf8(ran(&["check", "--help"], None).out).expect("the help is text"));
assert!(
verb.contains("senses whether each stream is a terminal"),
"--no-color's full description states the new default, not:\n{verb}"
);
assert!(
!verb.contains("already does"),
"--no-color's full description no longer claims this binary already writes no color"
);
}
fn report(extra: &[&str], columns: Option<&str>) -> Ran {
let root = repository();
let root = root.to_str().expect("the path is text").to_string();
let mut line: Vec<&str> = vec!["check", "--root", &root];
line.extend_from_slice(extra);
ran(&line, columns)
}
fn laid_out(report: &str) -> &str {
report
.split_once("\nread set\n")
.expect("the report carries a read-set block")
.0
}
#[test]
fn no_line_of_the_check_report_is_wider_than_eighty_columns() {
let ran = report(&[], None);
assert_eq!(ran.code, Some(0), "the report ran");
let out = String::from_utf8(ran.out).expect("the report is text");
let body = laid_out(&out);
let mut avoidable: Vec<String> = Vec::new();
let mut unfoldable: Vec<String> = Vec::new();
let mut lines = 0;
let mut widest = (0usize, String::new());
for (at, line) in body.lines().enumerate() {
lines += 1;
let width = line.chars().count();
if width <= WIDTH {
continue;
}
let entry = format!("line {}, {width} columns: {line}", at + 1);
match headwater_check::fill::unfoldable(line, WIDTH) {
true => unfoldable.push(line.to_string()),
false => {
if width > widest.0 {
widest = (width, line.to_string());
}
avoidable.push(entry);
}
}
}
assert!(
avoidable.is_empty(),
"{} of {lines} laid-out lines are wider than {WIDTH} columns and the fill could have \
narrowed every one. The widest is {} columns:\n{}\n\nEvery one:\n{}",
avoidable.len(),
widest.0,
widest.1,
avoidable.join("\n")
);
assert!(
!unfoldable.is_empty(),
"no line of the report carries an unbreakable word, so the exemption \
above is no longer measuring anything and should be deleted"
);
println!(
"{} of {lines} laid-out lines are over {WIDTH} columns, every one because a single word \
of it already is",
unfoldable.len()
);
for line in unfoldable.iter().map(String::as_str) {
let longest = line
.split_whitespace()
.map(|word| word.chars().count())
.max()
.expect("the line has a word");
assert!(
line.chars().count() - longest <= WIDTH,
"an unfoldable line that is also too long without its long word: {line}"
);
}
}
#[test]
fn no_finding_states_its_severity_on_a_line_of_its_own() {
for columns in [None, Some("100"), Some("120")] {
let extra: &[&str] = match columns {
None => &[],
Some(_) => &["--wide"],
};
let ran = report(extra, columns);
assert_eq!(ran.code, Some(0), "the report ran");
let out = String::from_utf8(ran.out).expect("the report is text");
let asked = columns.unwrap_or("80");
for (at, line) in out.lines().enumerate() {
assert!(
!matches!(
line.trim(),
"error" | "warn" | "info" | "✗ error" | "▲ warn" | "· info"
),
"at COLUMNS={asked}, line {} of the report is a bare severity word, so a \
finding's location and its severity are on two lines:\n{}",
at + 1,
out.lines()
.skip(at.saturating_sub(2))
.take(5)
.collect::<Vec<&str>>()
.join("\n")
);
}
let located = out
.lines()
.filter(|line| {
let trimmed = line.trim_end();
trimmed.split_whitespace().count() == 3
&& (trimmed.ends_with(" ✗ error")
|| trimmed.ends_with(" ▲ warn")
|| trimmed.ends_with(" · info"))
})
.count();
assert!(
located > 0,
"at COLUMNS={asked} no finding location line was found, so this case is \
measuring nothing"
);
}
}
#[test]
fn the_read_set_block_of_the_report_is_the_artifact_the_flag_writes() {
let at = std::env::temp_dir().join(format!("headwater-readset-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("the directory is there");
let path = at.join("read-set");
let written = path.to_str().expect("the path is text").to_string();
let ran = report(&["--read-set", &written], None);
assert_eq!(ran.code, Some(0), "the report ran");
let out = String::from_utf8(ran.out).expect("the report is text");
let file = std::fs::read_to_string(&path).expect("the flag wrote the file");
assert!(!file.is_empty(), "the file has a read set in it");
let block = out
.split_once("\nread set\n")
.expect("the report carries a read-set block")
.1;
let indented: String = file
.lines()
.map(|line| match line.is_empty() {
true => String::from("\n"),
false => format!(" {line}\n"),
})
.collect();
assert_eq!(
block, indented,
"the read-set block of the report is not the file the flag wrote"
);
}
#[test]
fn a_report_that_states_columns_and_one_that_does_not_write_the_same_bytes() {
let bare = report(&[], None);
assert_eq!(bare.code, Some(0));
for absurd in ["1", "40", "500", "100000", "not a number"] {
let stated = report(&[], Some(absurd));
assert_eq!(
bare.out, stated.out,
"the report under COLUMNS={absurd} is not the report with none"
);
assert_eq!(bare.code, stated.code);
}
}
#[test]
fn the_width_a_caller_asks_for_lays_the_report_out_and_is_held_to_the_band() {
let text = |ran: Ran| String::from_utf8(ran.out).expect("the report is text");
let ordinary = text(report(&[], None));
let wide = |columns: &str| text(report(&["--wide"], Some(columns)));
let narrow = wide("40");
assert_eq!(narrow, ordinary, "a width under {WIDTH} is {WIDTH}");
assert_eq!(wide("80"), ordinary);
let widest_asked = wide("500");
assert_eq!(
widest_asked,
wide("120"),
"a width over {WIDEST} is {WIDEST}"
);
assert_ne!(widest_asked, ordinary, "{WIDEST} is not {WIDTH}");
let between = wide("100");
assert_ne!(between, ordinary);
assert_ne!(between, widest_asked);
for (asked, out) in [(WIDTH, &narrow), (100, &between), (WIDEST, &widest_asked)] {
for line in laid_out(out).lines() {
assert!(
line.chars().count() <= asked || headwater_check::fill::unfoldable(line, asked),
"at {asked} columns this line is {} and the fill could have narrowed it: {line}",
line.chars().count()
);
}
}
}
#[test]
fn a_report_written_to_a_pipe_a_file_and_a_terminal_is_the_same_bytes() {
let root = repository();
let root = root.to_str().expect("the path is text").to_string();
let binary = env!("CARGO_BIN_EXE_headwater");
let at = std::env::temp_dir().join(format!("headwater-streams-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("the directory is there");
let piped = report(&[], None);
assert_eq!(piped.code, Some(0), "the piped run exits 0");
let file = at.join("report");
let shell = Process::new("sh")
.arg("-c")
.arg(format!(
"'{binary}' check --root '{root}' > '{}'",
file.display()
))
.current_dir(&at)
.env_remove("COLUMNS")
.output()
.expect("the shell runs");
assert_eq!(shell.status.code(), Some(0), "the redirected run exits 0");
let written = std::fs::read(&file).expect("the shell wrote the file");
assert_eq!(
piped.out, written,
"a pipe and a file are not the same bytes"
);
let found = Process::new("sh")
.arg("-c")
.arg("command -v script")
.output()
.map(|out| out.status.success())
.unwrap_or(false);
if !found {
println!("skipped: `script` is not on PATH, so no terminal leg ran");
return;
}
let under = at.join("terminal");
let pty = Process::new("script")
.args([
"-qec",
&format!("'{binary}' check --root '{root}' > '{}'", under.display()),
"/dev/null",
])
.current_dir(&at)
.env_remove("COLUMNS")
.output()
.expect("`script` runs");
assert!(pty.status.success(), "the run under a terminal exits 0");
let seen: Vec<u8> = std::fs::read(&under)
.expect("the run under a terminal wrote the file")
.into_iter()
.filter(|byte| *byte != b'\r')
.collect();
assert_eq!(
piped.out, seen,
"a pipe and a terminal are not the same bytes"
);
}