use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Output};
fn write_description(name: &str, contents: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"monospace-cli-test-{name}-{}.json",
std::process::id()
));
std::fs::File::create(&path)
.and_then(|mut file| file.write_all(contents.as_bytes()))
.expect("temp description file should be writable");
path
}
fn run(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_monospace-cli"))
.args(args)
.output()
.expect("the monospace-cli binary should be runnable")
}
const DEMO_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/demo.json");
fn first_demonstrated_picture(output: &str) -> String {
let (first_block, _rest) = output
.split_once("\n\n")
.expect("two captioned pictures separated by a blank line");
let (_caption, picture) = first_block
.split_once('\n')
.expect("a caption line precedes the picture");
format!("{picture}\n")
}
#[test]
fn an_identity_written_as_a_string_is_refused_by_name_on_stderr_and_fails() {
let a_string_id = write_description(
"string-id",
r##"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": "#1", "at": { "x": 0, "y": 0 },
"size": { "width": 4, "height": 3 }, "stroke": "light" } ]
}"##,
);
let a_string_shape = write_description(
"string-shape",
r##"{
"next_id": 3,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light" },
{ "kind": "connector", "id": 2,
"from": { "at": { "kind": "reference", "shape": "#1", "anchor": "right" },
"terminal": { "kind": "arm" } },
"to": { "at": { "kind": "point", "x": 8, "y": 1 }, "leaving": "left",
"terminal": { "kind": "arm" } },
"stroke": "light" } ]
}"##,
);
for (path, named) in [(a_string_id, "id"), (a_string_shape, "shape")] {
let output = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
assert!(
!output.status.success(),
"a name is not an ordinal and must fail"
);
assert!(output.stdout.is_empty(), "printed to stdout");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(&format!("`{named}` is an ordinal")),
"expected `{named}` to be named on stderr, got: {stderr}"
);
assert!(
stderr.contains("\"#1\""),
"expected the message to quote what the file said, got: {stderr}"
);
}
}
#[test]
fn a_next_id_of_zero_is_refused_by_name_on_stderr_and_fails() {
let path = write_description(
"zero-next-id",
r#"{
"next_id": 0,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 0, "y": 0 },
"size": { "width": 4, "height": 3 }, "stroke": "light" } ]
}"#,
);
let output = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
assert!(
!output.status.success(),
"zero is not an identity and must fail"
);
assert!(output.stdout.is_empty(), "printed to stdout");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("`next_id` is an ordinal"),
"expected `next_id` to be named on stderr, got: {stderr}"
);
}
#[test]
fn two_entries_carrying_one_identity_are_both_read_and_both_drawn() {
let path = write_description(
"repeated-id",
r#"{
"next_id": 2,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light" },
{ "kind": "box", "id": 1, "at": { "x": 2, "y": 1 }, "size": { "width": 4, "height": 3 },
"stroke": "light" }
]
}"#,
);
let output = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
assert!(output.status.success(), "exited with {}", output.status);
assert!(output.stderr.is_empty(), "wrote to stderr");
let picture = String::from_utf8_lossy(&output.stdout);
assert!(
picture.contains("┌──┐") && picture.contains("└──┘") && picture.lines().count() >= 4,
"both entries must draw, and a 6x4 window is four rows:\n{picture}"
);
}
#[test]
fn no_arguments_prints_the_demonstration_and_exits_successfully() {
let output = run(&[]);
assert!(output.status.success(), "exited with {}", output.status);
assert!(!output.stdout.is_empty(), "printed nothing");
assert!(output.stderr.is_empty(), "wrote to stderr");
}
#[test]
fn running_from_a_different_working_directory_prints_the_same_diagram() {
let from_temp_dir = Command::new(env!("CARGO_BIN_EXE_monospace-cli"))
.current_dir(std::env::temp_dir())
.output()
.expect("the monospace-cli binary should be runnable from the temp directory");
let from_manifest_dir = Command::new(env!("CARGO_BIN_EXE_monospace-cli"))
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output()
.expect("the monospace-cli binary should be runnable from its manifest directory");
assert_eq!(from_temp_dir.stdout, from_manifest_dir.stdout);
}
#[test]
fn the_demo_path_passed_explicitly_prints_the_demonstrations_first_picture() {
let no_arguments = run(&[]);
let explicit_path = run(&[DEMO_PATH]);
assert!(no_arguments.status.success());
assert!(explicit_path.status.success());
assert_eq!(
String::from_utf8_lossy(&explicit_path.stdout),
first_demonstrated_picture(&String::from_utf8_lossy(&no_arguments.stdout))
);
}
#[test]
fn an_explicit_path_prints_the_hand_written_box() {
let path = write_description(
"one-box",
r#"{
"next_id": 2,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" }
]
}"#,
);
let output = run(&[
"--size",
"4x3",
path.to_str().expect("temp path should be valid UTF-8"),
]);
assert!(output.status.success(), "exited with {}", output.status);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"┌──┐\n│░░│\n└──┘\n"
);
assert!(output.stderr.is_empty(), "wrote to stderr");
}
#[test]
fn a_path_and_no_size_draws_into_the_demonstrations_window() {
let path = write_description(
"default-window",
r#"{
"next_id": 2,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" }
]
}"#,
);
let output = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
assert!(output.status.success(), "exited with {}", output.status);
let picture = String::from_utf8_lossy(&output.stdout);
assert_eq!(
picture.lines().count(),
17,
"the demonstration's window is seventeen rows tall: {picture}"
);
assert_eq!(
picture.lines().next().expect("a first row").chars().count(),
50,
"and fifty columns wide: {picture}"
);
assert!(
picture.starts_with("┌──┐"),
"the box stands in the window's own top-left corner: {picture}"
);
}
#[test]
fn the_bare_run_draws_into_the_demonstrations_window() {
let bare = run(&[]);
let asked = run(&["--size", "6x4", "--origin", "1,1"]);
for output in [&bare, &asked] {
assert!(output.status.success(), "exited with {}", output.status);
let stdout = String::from_utf8_lossy(&output.stdout);
let first = first_demonstrated_picture(&stdout);
assert_eq!(
first.lines().count(),
17,
"the demonstration is seventeen rows tall whatever the command line says"
);
assert_eq!(
first.lines().next().expect("a first row").chars().count(),
50,
"and fifty columns wide"
);
}
}
#[test]
fn a_size_and_an_origin_on_the_command_line_decide_the_window_a_path_is_drawn_into() {
let path = write_description(
"with-flags",
r#"{
"next_id": 2,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" }
]
}"#,
);
let path = path.to_str().expect("temp path should be valid UTF-8");
let asked = run(&["--size", "4x3", path]);
assert!(asked.status.success(), "exited with {}", asked.status);
assert_eq!(
String::from_utf8_lossy(&asked.stdout),
"┌──┐\n│░░│\n└──┘\n",
"the command line's size is the window, not the file's own"
);
let moved = run(&["--size", "4x3", "--origin", "1,1", path]);
assert!(moved.status.success(), "exited with {}", moved.status);
assert_eq!(
String::from_utf8_lossy(&moved.stdout),
"░░│ \n──┘ \n \n",
"the origin is the window's corner, so the box at (0, 0) is outside it and only the part \
the window holds is drawn"
);
}
#[test]
fn the_window_comes_from_the_flags_and_nowhere_else() {
let path = write_description(
"two-windows",
r#"{
"canvas": { "origin": { "x": 0, "y": 0 }, "size": { "width": 13, "height": 9 } },
"next_id": 2,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" }
]
}"#,
);
let path = path.to_str().expect("temp path should be valid UTF-8");
let asked = run(&["--size", "6x4", path]);
assert!(asked.status.success(), "exited with {}", asked.status);
assert_eq!(
String::from_utf8_lossy(&asked.stdout),
"┌──┐ \n│░░│ \n└──┘ \n \n",
"the command line's window is the one the picture is drawn at"
);
let moved = run(&["--size", "6x4", "--origin", "1,1", path]);
assert!(moved.status.success(), "exited with {}", moved.status);
assert_eq!(
String::from_utf8_lossy(&moved.stdout),
"░░│ \n──┘ \n \n \n",
"the origin is the window's corner, so the box at (0, 0) is outside it and only the part \
the window holds is drawn"
);
}
#[test]
fn a_flag_this_binary_does_not_read_names_it_on_stderr_and_fails() {
let output = run(&["--width", "20", "x.json"]);
assert!(!output.status.success());
assert!(output.stdout.is_empty(), "printed to stdout");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("`--width` is not an option"), "{stderr}");
assert!(stderr.contains("usage:"), "{stderr}");
}
#[test]
fn a_file_with_a_box_a_line_and_a_connector_prints_all_three_composed() {
let path = write_description(
"three-shapes",
r#"{
"next_id": 4,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" },
{ "kind": "line", "id": 2, "at": { "x": 0, "y": 4 }, "len": 4, "orientation": "horizontal",
"stroke": "light" },
{ "kind": "connector", "id": 3,
"from": { "at": { "kind": "point", "x": 5, "y": 0 }, "leaving": "right",
"terminal": { "kind": "glyph", "glyph": ">" } },
"to": { "at": { "kind": "point", "x": 9, "y": 2 }, "leaving": "down",
"terminal": { "kind": "glyph", "glyph": "v" } },
"stroke": "light" }
]
}"#,
);
let output = run(&[
"--size",
"10x5",
path.to_str().expect("temp path should be valid UTF-8"),
]);
assert!(output.status.success(), "exited with {}", output.status);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"┌──┐ >─┐ \n│░░│ │ \n└──┘ │ v\n └─┘\n──── \n"
);
assert!(output.stderr.is_empty(), "wrote to stderr");
}
const A_BOX: &str = r#"{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light" }"#;
fn picture_of_a_connector_hanging_from(label: &str, at: &str) -> String {
let leaving = if at.contains(r#""kind": "point""#) {
r#""leaving": "right","#
} else {
""
};
let path = write_description(
label,
&format!(
r#"{{
"next_id": 3,
"shapes": [
{A_BOX},
{{ "kind": "connector", "id": 2,
"from": {{ "at": {at}, {leaving}
"terminal": {{ "kind": "glyph", "glyph": ">" }} }},
"to": {{ "at": {{ "kind": "point", "x": 8, "y": 1 }}, "leaving": "left",
"terminal": {{ "kind": "arm" }} }},
"stroke": "light" }}
]
}}"#
),
);
let output = run(&[
"--size",
"9x3",
path.to_str().expect("temp path should be valid UTF-8"),
]);
assert!(output.status.success(), "exited with {}", output.status);
assert!(output.stderr.is_empty(), "wrote to stderr");
String::from_utf8_lossy(&output.stdout).into_owned()
}
#[test]
fn a_file_naming_a_reference_draws_what_the_point_it_resolves_to_draws() {
let named = picture_of_a_connector_hanging_from(
"with-offset",
r#"{ "kind": "reference", "shape": 1, "anchor": "right", "offset": { "dx": 2, "dy": 0 } }"#,
);
let spelled = picture_of_a_connector_hanging_from(
"with-offset-point",
r#"{ "kind": "point", "x": 6, "y": 1 }"#,
);
assert_eq!(named, spelled);
assert!(
!named.trim().is_empty(),
"a reference to a box the file holds must draw the connector"
);
}
#[test]
fn a_file_naming_a_reference_with_no_offset_draws_the_point_it_stands_on() {
let named = picture_of_a_connector_hanging_from(
"no-offset",
r#"{ "kind": "reference", "shape": 1, "anchor": "right" }"#,
);
let spelled = picture_of_a_connector_hanging_from(
"no-offset-point",
r#"{ "kind": "point", "x": 4, "y": 1 }"#,
);
assert_eq!(named, spelled);
}
#[test]
fn a_file_naming_an_out_draws_the_point_the_offset_it_stands_for_draws() {
let named = picture_of_a_connector_hanging_from(
"with-out",
r#"{ "kind": "reference", "shape": 1, "anchor": "right", "out": 1 }"#,
);
let as_an_offset = picture_of_a_connector_hanging_from(
"with-the-offset-an-out-stands-for",
r#"{ "kind": "reference", "shape": 1, "anchor": "right", "offset": { "dx": 1, "dy": 0 } }"#,
);
let outright = picture_of_a_connector_hanging_from(
"with-the-point-an-out-reaches",
r#"{ "kind": "point", "x": 5, "y": 1 }"#,
);
let at_default_gap = picture_of_a_connector_hanging_from(
"with-no-gap",
r#"{ "kind": "reference", "shape": 1, "anchor": "right" }"#,
);
assert_eq!(
named, as_an_offset,
"an `out` and the offset it stands for differ"
);
assert_eq!(named, outright, "an `out` and the point it reaches differ");
assert_ne!(
named, at_default_gap,
"an `out` of one drew what a reference with no gap draws, so the field is being dropped"
);
}
#[test]
fn a_file_naming_a_shape_it_does_not_hold_draws_the_box_and_no_connector_and_succeeds() {
let drawn = picture_of_a_connector_hanging_from(
"foreign-shape",
r#"{ "kind": "reference", "shape": 7, "anchor": "right", "offset": { "dx": 2, "dy": 0 } }"#,
);
let box_alone = {
let path = write_description(
"just-the-box",
&format!(
r#"{{
"next_id": 2,
"shapes": [ {A_BOX} ]
}}"#
),
);
let output = run(&[
"--size",
"9x3",
path.to_str().expect("temp path should be valid UTF-8"),
]);
assert!(output.status.success(), "exited with {}", output.status);
assert!(output.stderr.is_empty(), "wrote to stderr");
String::from_utf8_lossy(&output.stdout).into_owned()
};
assert_eq!(
drawn, box_alone,
"a reference to nothing must draw the box and nothing else"
);
}
fn overlap_boxes() -> (monospace_core::BoxShape, monospace_core::BoxShape) {
use monospace_core::{BoxShape, Glyph, Pos, Size, Stroke};
(
BoxShape {
at: Pos { x: 0, y: 0 },
size: Size {
width: 4,
height: 3,
},
stroke: Stroke::from("light"),
fill: Some(Glyph::new("░").expect("\"░\" is one glyph")),
},
BoxShape {
at: Pos { x: 2, y: 1 },
size: Size {
width: 4,
height: 3,
},
stroke: Stroke::from("light"),
fill: Some(Glyph::new("▓").expect("\"▓\" is one glyph")),
},
)
}
fn render_back_to_front_with_above(
back: &monospace_core::BoxShape,
front: &monospace_core::BoxShape,
) -> String {
use monospace_core::{
Buffer, FigureLayer, GlyphCatalog, Pos, Shape, Size, StampMode, Window, render,
};
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 6,
height: 4,
};
let mut buffer = Buffer::new(Window { origin, size });
back.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
front.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
render(&buffer, &GlyphCatalog::light(), buffer.window())
}
#[test]
fn reordering_shapes_changes_which_one_is_drawn_on_top() {
let first = write_description(
"overlap-a-then-b",
r#"{
"next_id": 3,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" },
{ "kind": "box", "id": 2, "at": { "x": 2, "y": 1 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "▓" }
]
}"#,
);
let second = write_description(
"overlap-b-then-a",
r#"{
"next_id": 3,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 2, "y": 1 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "▓" },
{ "kind": "box", "id": 2, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" }
]
}"#,
);
let first_output = run(&[
"--size",
"6x4",
first.to_str().expect("temp path should be valid UTF-8"),
]);
let second_output = run(&[
"--size",
"6x4",
second.to_str().expect("temp path should be valid UTF-8"),
]);
let (a, b) = overlap_boxes();
assert_eq!(
String::from_utf8_lossy(&first_output.stdout),
render_back_to_front_with_above(&a, &b)
);
assert_eq!(
String::from_utf8_lossy(&second_output.stdout),
render_back_to_front_with_above(&b, &a)
);
assert_ne!(first_output.stdout, second_output.stdout);
}
#[test]
fn running_the_same_file_twice_produces_identical_output() {
let path = write_description(
"determinism",
r#"{
"next_id": 2,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 4, "height": 3 },
"stroke": "light", "fill": "░" }
]
}"#,
);
let first = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
let second = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
assert_eq!(first.stdout, second.stdout);
}
#[test]
fn a_missing_path_names_it_on_stderr_and_fails() {
let path = std::env::temp_dir().join(format!(
"monospace-cli-test-does-not-exist-{}.json",
std::process::id()
));
let _ = std::fs::remove_file(&path);
let output = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
assert!(!output.status.success());
assert!(output.stdout.is_empty(), "printed to stdout");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(path.to_str().expect("temp path should be valid UTF-8")),
"{stderr}"
);
}
#[test]
fn malformed_json_locates_the_problem_on_stderr_and_fails() {
let path = write_description("broken", r#"{ "canvas": "#);
let output = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
assert!(!output.status.success());
assert!(output.stdout.is_empty(), "printed to stdout");
assert!(!output.stderr.is_empty(), "wrote nothing to stderr");
}
#[test]
fn an_unrecognized_kind_names_it_on_stderr_and_fails() {
let path = write_description(
"bad-kind",
r#"{
"next_id": 1,
"shapes": [ { "kind": "triangle" } ]
}"#,
);
let output = run(&[path.to_str().expect("temp path should be valid UTF-8")]);
assert!(!output.status.success());
assert!(output.stdout.is_empty(), "printed to stdout");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("triangle"), "{stderr}");
}
#[test]
fn more_than_one_argument_prints_usage_and_fails() {
let output = run(&["one.json", "two.json"]);
assert!(!output.status.success());
assert!(output.stdout.is_empty(), "printed to stdout");
assert!(!output.stderr.is_empty(), "wrote nothing to stderr");
}
fn char_at(output: &str, x: usize, y: usize) -> char {
output
.lines()
.nth(y + 1)
.and_then(|line| line.chars().nth(x))
.unwrap_or_else(|| panic!("no character at ({x}, {y}) in {output:?}"))
}
#[test]
fn the_demonstration_contains_both_light_and_ascii_characters() {
let output = run(&[]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(['┌', '┐', '└', '┘', '│', '─', '┬', '┴', '├', '┤', '┼']),
"no box-drawing character in {stdout:?}"
);
assert!(
stdout.contains(['+', '-', '|']),
"no ASCII box character in {stdout:?}"
);
}
#[test]
fn the_two_crossings_read_from_whichever_figure_is_in_front() {
let output = run(&[]);
let stdout = String::from_utf8_lossy(&output.stdout);
let ascii_in_front = char_at(&stdout, 28, 2);
assert!(
matches!(ascii_in_front, '+' | '-' | '|'),
"expected an ASCII character at (28, 2), got {ascii_in_front:?}"
);
let light_in_front = char_at(&stdout, 36, 2);
assert_eq!(
light_in_front, '┼',
"expected the Light crossing character at (36, 2)"
);
}
#[test]
fn the_demonstration_contains_double_heavy_and_light_round_characters() {
let output = run(&[]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(['║', '╗', '╦', '╬', '╣', '╔', '╠', '═', '╩', '╝', '╚']),
"no Double box character in {stdout:?}"
);
assert!(
stdout.contains(['┃', '┓', '┳', '╋', '┫', '┏', '┣', '━', '┻', '┛', '┗']),
"no Heavy box character in {stdout:?}"
);
assert!(
stdout.contains(['╮', '╭', '╯', '╰']),
"no Light Round corner character in {stdout:?}"
);
}
#[test]
fn crossings_between_the_new_tables_mix_or_degrade_depending_on_table_coverage() {
let output = run(&[]);
let stdout = String::from_utf8_lossy(&output.stdout);
for (x, y, expected, label) in [
(
2,
11,
'╫',
"Double in front of Light: covered by Light/Double, mixes instead of degrading",
),
(
10,
11,
'╪',
"Light in front of Double: covered by Light/Double, mixes instead of degrading",
),
(
18,
11,
'╂',
"Heavy in front of Light: covered by Light/Heavy, mixes instead of degrading",
),
(
26,
11,
'┿',
"Light in front of Heavy: covered by Light/Heavy, mixes instead of degrading",
),
(
34,
11,
'┼',
"Light Round in front of Light: no mixing table pairs them, still degrades",
),
(
42,
11,
'╬',
"Double in front of Heavy: no mixing table pairs them, still degrades",
),
] {
let ch = char_at(&stdout, x, y);
assert_eq!(ch, expected, "{label}: expected {expected:?} at ({x}, {y})");
}
}
#[test]
fn symbols_prints_a_row_per_style_in_name_order_with_a_space_for_a_miss() {
let output = run(&["--symbols"]);
assert!(output.status.success(), "the sheet is a run");
assert!(output.stderr.is_empty(), "wrote to stderr");
let expected = concat!(
"arrow ↑→↓← \n",
"ascii ^>v< o\n",
"outline △▷▽◁◇⬡○\n",
"small-outline ▵▹▿◃ \n",
"small-solid ▴▸▾◂ \n",
"solid ▲▶▼◀◆⬢●\n",
"solid-pointer ▲►▼◄ \n",
);
assert_eq!(String::from_utf8_lossy(&output.stdout), expected);
}
#[test]
fn symbols_refuses_a_path_and_a_window_and_the_usage_line_names_it() {
for (args, said) in [
(
&["--symbols", "x.json"][..],
"`x.json` cannot be given beside `--symbols`",
),
(
&["--size", "4x3", "--symbols"][..],
"`--size` cannot be given beside `--symbols`",
),
(
&["--origin", "0,0", "--symbols"][..],
"`--origin` cannot be given beside `--symbols`",
),
(
&["--symbols", "--symbols"][..],
"`--symbols` was given twice",
),
] {
let output = run(args);
assert!(
!output.status.success(),
"{args:?} should have been refused"
);
assert!(output.stdout.is_empty(), "{args:?} printed to stdout");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains(said), "{args:?}: {stderr}");
assert!(
stderr.contains("monospace-cli --symbols"),
"{args:?}: the usage line names the sheet: {stderr}"
);
}
}