use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
fn spritebox(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_spritebox"))
.args(args)
.output()
.expect("the binary runs")
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn temp_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("spritebox-cli-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write(dir: &Path, name: &str, text: &str) -> String {
let path = dir.join(name);
std::fs::write(&path, text).unwrap();
path.display().to_string()
}
const COIN: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/coin.spritebox");
const BAD: &str = "palette p\n k #000\nsprite s 2x1 palette=p\n kx\n";
#[test]
fn check_valid_file() {
let out = spritebox(&["check", COIN]);
assert_eq!(out.status.code(), Some(0));
assert!(stdout(&out).ends_with(": ok\n"), "{}", stdout(&out));
let out = spritebox(&["check", COIN, "--json"]);
assert_eq!(out.status.code(), Some(0));
let json = stdout(&out);
assert!(json.starts_with("{\"version\":1,\"file\":"), "{json}");
assert!(
json.contains("\"ok\":true,\"diagnostics\":[],\"written\":[]}"),
"{json}"
);
}
#[test]
fn check_invalid_file() {
let dir = temp_dir("invalid");
let file = write(&dir, "bad.spritebox", BAD);
let out = spritebox(&["check", &file]);
assert_eq!(out.status.code(), Some(1));
let text = stderr(&out);
assert!(
text.contains(&format!(
"{file}:2:5: error[invalid_argument]: Color `#000`"
)),
"{text}"
);
assert!(
text.contains(&format!("{file}:4:4: error[unknown_name]: Key `x`")),
"{text}"
);
assert!(text.ends_with(": 2 errors\n"), "{text}");
let out = spritebox(&["check", "--json", &file]);
assert_eq!(out.status.code(), Some(1));
let json = stdout(&out);
assert!(json.contains("\"ok\":false"), "{json}");
assert!(json.contains(
"{\"code\":\"invalid_argument\",\"line\":2,\"column\":5,\"message\":\"Color `#000` must be `#rrggbb`, `#rrggbbaa` or `transparent`\"}"
), "{json}");
assert!(
json.contains("{\"code\":\"unknown_name\",\"line\":4,\"column\":4,"),
"{json}"
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn check_reads_standard_input_and_rejects_bad_utf8() {
let mut child = Command::new(env!("CARGO_BIN_EXE_spritebox"))
.args(["check", "-", "--json"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"palette p\n k #000000\n \xff #000000\n")
.unwrap();
let out = child.wait_with_output().unwrap();
assert_eq!(out.status.code(), Some(1));
let json = stdout(&out);
assert!(json.contains("\"file\":\"<stdin>\""), "{json}");
assert!(json.contains("{\"code\":\"syntax\",\"line\":3,\"column\":3,\"message\":\"The text is not valid UTF-8\"}"), "{json}");
}
#[test]
fn render_writes_files() {
let dir = temp_dir("render");
let out = spritebox(&[
"render",
COIN,
"--out",
dir.to_str().unwrap(),
"--scale",
"2",
"--padding",
"2",
"--no-extrude",
"--json",
]);
assert_eq!(out.status.code(), Some(0), "{}", stderr(&out));
let json = stdout(&out);
for name in ["atlas.png", "spritebox.json", "preview.png"] {
assert!(
json.contains(&dir.join(name).display().to_string()),
"{json}"
);
}
let doc = spritebox::Document::parse(&std::fs::read_to_string(COIN).unwrap()).unwrap();
let options = spritebox::ExportOptions {
atlas: spritebox::AtlasOptions {
padding: 2,
extrude: false,
..Default::default()
},
};
let export = doc.export(&options).unwrap();
assert_eq!(
spritebox::png::read_file(dir.join("atlas.png")).unwrap(),
export.atlas.image
);
assert_eq!(
std::fs::read_to_string(dir.join("spritebox.json")).unwrap(),
export.to_json()
);
let preview = doc
.preview(&spritebox::PreviewOptions {
scale: 2,
..Default::default()
})
.unwrap();
assert_eq!(
spritebox::png::read_file(dir.join("preview.png")).unwrap(),
preview
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn render_writes_tilesets() {
let dir = temp_dir("render-tiles");
let tiles = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/tiles.spritebox");
let out = spritebox(&["render", tiles, "--out", dir.to_str().unwrap()]);
assert_eq!(out.status.code(), Some(0), "{}", stderr(&out));
let doc = spritebox::Document::parse(&std::fs::read_to_string(tiles).unwrap()).unwrap();
let export = doc.export(&spritebox::ExportOptions::default()).unwrap();
assert_eq!(
spritebox::png::read_file(dir.join("tileset-ground.png")).unwrap(),
export.tilesets[0].image
);
let text = stdout(&out);
let written: Vec<&str> = text
.lines()
.map(|l| l.rsplit('/').next().unwrap())
.collect();
assert_eq!(
written,
[
"atlas.png",
"tileset-ground.png",
"spritebox.json",
"preview.png"
]
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn font_writes_truetype() {
let dir = temp_dir("font");
let font = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/font.spritebox");
let path = dir.join("nested/hud.ttf");
let out = spritebox(&[
"font",
font,
"--out",
path.to_str().unwrap(),
"--family",
"HUD Pixel",
"--json",
]);
assert_eq!(out.status.code(), Some(0), "{}", stderr(&out));
let doc = spritebox::Document::parse(&std::fs::read_to_string(font).unwrap()).unwrap();
let expected = doc
.font("hud")
.unwrap()
.to_ttf(&spritebox::TtfOptions {
family: Some("HUD Pixel".into()),
..Default::default()
})
.unwrap();
assert_eq!(std::fs::read(&path).unwrap(), expected);
assert!(stdout(&out).contains("hud.ttf"));
let two = write(&dir, "two.spritebox", "font a height=1\nfont b height=1\n");
let ttf = dir.join("x.ttf");
let out = spritebox(&["font", &two, "--out", ttf.to_str().unwrap(), "--json"]);
assert_eq!(out.status.code(), Some(2));
assert!(
stdout(&out).contains("choose one with --font"),
"{}",
stdout(&out)
);
let out = spritebox(&["font", &two, "--out", ttf.to_str().unwrap(), "--font", "c"]);
assert_eq!(out.status.code(), Some(2));
let out = spritebox(&["font", &two, "--out", ttf.to_str().unwrap(), "--font", "b"]);
assert_eq!(out.status.code(), Some(0));
assert!(ttf.exists());
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn imports_are_read_from_disk_relative_to_the_file() {
let dir = temp_dir("imports");
std::fs::create_dir_all(dir.join("art/shared")).unwrap();
write(
&dir,
"art/shared/palettes.spritebox",
"palette p\n k #000000\n x #00\n",
);
let main = write(
&dir,
"art/main.spritebox",
"import shared/palettes.spritebox\nsprite s 1x1\n k\n",
);
let out = spritebox(&["check", &main]);
assert_eq!(out.status.code(), Some(1));
let expected = dir.join("art/shared/palettes.spritebox");
let text = stderr(&out);
assert!(
text.contains(&format!(
"{}:3:5: error[invalid_argument]",
expected.display()
)),
"{text}"
);
let out = spritebox(&["check", &main, "--json"]);
assert!(
stdout(&out).contains(&format!("\"file\":{:?}", expected.display().to_string())),
"{}",
stdout(&out)
);
write(
&dir,
"art/shared/palettes.spritebox",
"palette p\n k #000000\n",
);
let out = spritebox(&["render", &main, "--out", dir.join("out").to_str().unwrap()]);
assert_eq!(out.status.code(), Some(0), "{}", stderr(&out));
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn render_does_not_write_invalid_files() {
let dir = temp_dir("render-invalid");
let file = write(&dir, "bad.spritebox", BAD);
let out_dir = dir.join("out");
let out = spritebox(&["render", &file, "--out", out_dir.to_str().unwrap()]);
assert_eq!(out.status.code(), Some(1));
assert!(!out_dir.exists());
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn usage_and_io_errors() {
for args in [
&[][..],
&["draw", COIN],
&["check"],
&["check", COIN, "--frobnicate"],
&["check", COIN, COIN],
&["render", COIN],
&["render", COIN, "--out"],
&["render", COIN, "--out", "x", "--scale", "0"],
&["check", COIN, "--out", "x"],
&["check", COIN, "--no-extrude"],
&["render", COIN, "--out", "x", "--max-side", "0"],
&["font", COIN],
&["render", COIN, "--out", "x", "--font", "hud"],
&["render", COIN, "--out", "x", "--padding", "65"],
] {
let out = spritebox(args);
assert_eq!(out.status.code(), Some(2), "{args:?}");
assert!(stderr(&out).contains("Usage:"), "{args:?}");
}
let out = spritebox(&["check", "/no/such/file.spritebox", "--json"]);
assert_eq!(out.status.code(), Some(3));
assert!(stdout(&out).contains("\"code\":\"io\""), "{}", stdout(&out));
let out = spritebox(&["--help"]);
assert_eq!(out.status.code(), Some(0));
assert!(stdout(&out).contains("spritebox check <file>"));
let out = spritebox(&["--version"]);
assert_eq!(
stdout(&out),
format!("spritebox {}\n", env!("CARGO_PKG_VERSION"))
);
}