use std::io::Write;
use std::process::{Command, Stdio};
fn kuva_bin() -> Command {
let bin = env!("CARGO_BIN_EXE_kuva");
Command::new(bin)
}
fn run_with_stdin(args: &[&str], input: &str) -> (String, String, i32) {
let mut cmd = kuva_bin();
cmd.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("failed to spawn kuva");
child
.stdin
.take()
.expect("stdin")
.write_all(input.as_bytes())
.expect("write stdin");
let out = child.wait_with_output().expect("wait");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
out.status.code().unwrap_or(-1),
)
}
const MONTHLY_TSV: &str = "date\tvalue\n\
2024-01-01\t10\n\
2024-02-15\t14\n\
2024-03-10\t9\n\
2024-04-22\t18\n\
2024-06-01\t25\n";
#[test]
fn line_auto_mode_produces_month_year_ticks() {
let (stdout, stderr, code) = run_with_stdin(
&[
"line",
"--x",
"date",
"--y",
"value",
"--x-date-format",
"%Y-%m-%d",
],
MONTHLY_TSV,
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("Jan 2024") && stdout.contains("Jun 2024"),
"auto mode should pick a months+year format for a ~5-month range: {stdout}"
);
}
#[test]
fn scatter_explicit_unit_and_tick_format() {
let (stdout, stderr, code) = run_with_stdin(
&[
"scatter",
"--x",
"date",
"--y",
"value",
"--x-date-format",
"%Y-%m-%d",
"--x-date-unit",
"months",
"--x-date-tick-format",
"%b %y",
],
MONTHLY_TSV,
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains(">Jan 24<") || stdout.contains(">Jan 24 <"),
"explicit tick format '%b %y' should render as e.g. 'Jan 24': {stdout}"
);
assert!(
!stdout.contains("Jan 2024"),
"explicit tick format should override the auto default: {stdout}"
);
}
#[test]
fn tick_step_widens_tick_spacing() {
let (default_ticks, _, code1) = run_with_stdin(
&[
"line",
"--x",
"date",
"--y",
"value",
"--x-date-format",
"%Y-%m-%d",
"--x-date-unit",
"months",
],
MONTHLY_TSV,
);
let (stepped_ticks, _, code2) = run_with_stdin(
&[
"line",
"--x",
"date",
"--y",
"value",
"--x-date-format",
"%Y-%m-%d",
"--x-date-unit",
"months",
"--x-date-tick-step",
"2",
],
MONTHLY_TSV,
);
assert_eq!(code1, 0);
assert_eq!(code2, 0);
let count = |svg: &str| svg.matches("text-anchor=\"middle\">").count();
assert!(
count(&stepped_ticks) < count(&default_ticks),
"every-2-months should produce fewer x ticks than every-1-month \
(stepped {} vs default {})",
count(&stepped_ticks),
count(&default_ticks)
);
}
#[test]
fn unparseable_date_reports_a_clear_error() {
let (_stdout, stderr, code) = run_with_stdin(
&[
"line",
"--x",
"date",
"--y",
"value",
"--x-date-format",
"%d/%m/%Y",
],
MONTHLY_TSV,
);
assert_ne!(
code, 0,
"should fail: input dates don't match the given format"
);
assert!(
stderr.contains("cannot parse") && stderr.contains("2024-01-01"),
"error should name the offending value: {stderr}"
);
}
#[test]
fn datetime_x_axis_works_with_color_by() {
let tsv = "date\tvalue\tgroup\n\
2024-01-01\t10\tA\n\
2024-02-01\t12\tA\n\
2024-01-15\t20\tB\n\
2024-02-15\t22\tB\n";
let (stdout, stderr, code) = run_with_stdin(
&[
"scatter",
"--x",
"date",
"--y",
"value",
"--color-by",
"group",
"--x-date-format",
"%Y-%m-%d",
],
tsv,
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("<svg"),
"should produce valid SVG: {stdout}"
);
}
#[test]
fn datetime_x_axis_works_with_multi_y() {
let tsv = "date\ta\tb\n\
2024-01-01\t10\t5\n\
2024-02-01\t12\t7\n\
2024-03-01\t9\t6\n";
let (stdout, stderr, code) = run_with_stdin(
&[
"line",
"--x",
"date",
"--y",
"a,b",
"--x-date-format",
"%Y-%m-%d",
],
tsv,
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("<svg"),
"should produce valid SVG: {stdout}"
);
}
#[test]
fn without_x_date_format_x_column_is_still_parsed_as_a_plain_number() {
let tsv = "x\ty\n1\t2\n3\t4\n5\t3\n";
let (stdout, stderr, code) = run_with_stdin(&["scatter", "--x", "x", "--y", "y"], tsv);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(stdout.contains("<svg"));
}