use std::path::PathBuf;
use ironlab::ir::IssueKind;
use ironlab::prelude::*;
fn temp_path(name: &str) -> PathBuf {
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("ironlab-facade-tests");
std::fs::create_dir_all(&dir).expect("create temporary directory");
dir.join(name)
}
fn sample_figure() -> Figure {
let x = linspace(0.0, 1.0, 5);
let z = Matrix::from_fn(x.len(), x.len(), |row, col| (row * col) as f64);
let mut fig = Figure::new()
.tiles(1, 2)
.title("Sample")
.parameter("converged", true)
.parameter("cells", -25)
.parameter("reynolds_number", 1e5)
.parameter("solver", "k–ω SST");
fig.axes(0, 0)
.plot(&x, &x)
.display_name("$y = x$")
.marker(Marker::Circle);
fig.axes(0, 0).legend(LegendLocation::NorthWest);
fig.axes(0, 1).surf(&x, &x, &z);
fig.link_all_x().unwrap();
fig
}
fn fresh(name: &str) -> PathBuf {
let path = temp_path(name);
let _ = std::fs::remove_file(&path);
path
}
#[test]
fn save_then_load_round_trips_a_fig_file() {
let fig = sample_figure();
let path = fresh("round_trip.fig");
fig.save(&path).unwrap();
let loaded = Figure::load(&path).unwrap();
assert_eq!(loaded, fig);
}
#[test]
fn save_then_load_round_trips_json_files() {
let fig = sample_figure();
for name in ["round_trip.fig.json", "round_trip.json"] {
let path = fresh(name);
fig.save(&path).unwrap();
let loaded = Figure::load(&path).unwrap();
assert_eq!(loaded, fig, "{name}");
}
}
#[test]
fn save_writes_protobuf_to_a_fig_file() {
let fig = sample_figure();
let path = fresh("dispatch.fig");
fig.save(&path).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), fig.ir().to_protobuf());
}
#[test]
fn save_writes_json_to_a_json_file() {
let fig = sample_figure();
for name in ["dispatch.fig.json", "dispatch.json"] {
let path = fresh(name);
fig.save(&path).unwrap();
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
fig.ir().to_json(),
"{name}"
);
}
}
#[test]
fn load_reads_each_format_by_extension() {
let fig = sample_figure();
let fig_path = fresh("load_dispatch.fig");
std::fs::write(&fig_path, fig.to_protobuf()).unwrap();
assert_eq!(Figure::load(&fig_path).unwrap(), fig);
let json_path = fresh("load_dispatch.fig.json");
std::fs::write(&json_path, fig.ir().to_json()).unwrap();
assert_eq!(Figure::load(&json_path).unwrap(), fig);
}
#[test]
fn extensions_are_matched_without_regard_to_case() {
let fig = sample_figure();
let path = fresh("upper_case.FIG");
fig.save(&path).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), fig.ir().to_protobuf());
assert_eq!(Figure::load(&path).unwrap(), fig);
}
#[test]
fn save_refuses_an_unsupported_extension_without_writing() {
for name in ["figure.txt", "figure", "figure.fig.bak"] {
let path = fresh(name);
let error = sample_figure().save(&path).unwrap_err();
assert!(
matches!(&error, Error::UnsupportedFormat(p) if p == &path),
"{name}: {error:?}"
);
let message = error.to_string();
assert!(
message.contains(".fig") && message.contains(".json"),
"the message must name the supported extensions: {message}"
);
assert!(!path.exists(), "{name} was written");
}
}
#[test]
fn load_refuses_an_unsupported_extension_before_reading() {
let path = fresh("missing.txt");
assert!(matches!(
Figure::load(&path),
Err(Error::UnsupportedFormat(p)) if p == path
));
}
#[test]
fn save_json_and_load_json_ignore_the_extension() {
let fig = sample_figure();
let path = fresh("explicit.txt");
fig.save_json(&path).unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), fig.ir().to_json());
assert_eq!(Figure::load_json(&path).unwrap(), fig);
}
#[test]
fn protobuf_bytes_round_trip() {
let fig = sample_figure();
let bytes = fig.to_protobuf();
assert_eq!(bytes, fig.ir().to_protobuf());
assert_eq!(Figure::from_protobuf(&bytes).unwrap(), fig);
}
#[test]
fn a_loaded_figure_can_be_extended() {
let path = fresh("extend.fig");
sample_figure().save(&path).unwrap();
let mut loaded = Figure::load(&path).unwrap();
loaded.axes(0, 0).plot([0.0, 1.0], [1.0, 0.0]);
let report = loaded.validate();
assert!(
!report
.errors
.iter()
.any(|issue| issue.kind == IssueKind::DuplicateNodeId),
"{report:?}"
);
}
#[test]
fn load_rejects_a_file_that_is_not_a_figure() {
let fig_path = fresh("garbage.fig");
std::fs::write(&fig_path, sample_figure().ir().to_json()).unwrap();
assert!(matches!(Figure::load(&fig_path), Err(Error::Ir(_))));
let json_path = fresh("garbage.fig.json");
std::fs::write(&json_path, b"this is not json {").unwrap();
assert!(matches!(Figure::load(&json_path), Err(Error::Ir(_))));
}
#[test]
fn load_reports_a_missing_file_as_io() {
for name in ["does_not_exist.fig", "does_not_exist.fig.json"] {
let path = fresh(name);
assert!(matches!(Figure::load(&path), Err(Error::Io(_))), "{name}");
}
}
#[test]
fn export_pdf_writes_a_pdf_file() {
let path = temp_path("sample.pdf");
let _ = std::fs::remove_file(&path);
sample_figure().export_pdf(&path).unwrap();
let bytes = std::fs::read(&path).unwrap();
assert!(bytes.starts_with(b"%PDF"), "file does not start with %PDF");
}
#[test]
fn export_pdf_refuses_an_invalid_figure() {
let path = temp_path("invalid.pdf");
let _ = std::fs::remove_file(&path);
let mut fig = Figure::new();
fig.axes(0, 0).plot([0.0, 1.0, 2.0], [0.0]);
let error = fig.export_pdf(&path).unwrap_err();
match &error {
Error::Invalid(report) => {
assert!(!report.is_valid());
let message = error.to_string();
for issue in &report.errors {
assert!(message.contains(&issue.message), "{message}");
}
}
other => panic!("expected Error::Invalid, found {other:?}"),
}
assert!(!path.exists());
}
#[test]
fn export_pdf_to_a_missing_directory_is_an_io_error() {
let path = temp_path("no_such_directory").join("figure.pdf");
let _ = std::fs::remove_dir_all(path.parent().unwrap());
let result = sample_figure().export_pdf(&path);
assert!(matches!(result, Err(Error::Io(_))), "{result:?}");
}
#[test]
fn show_refuses_an_invalid_figure_without_opening_a_window() {
let mut fig = Figure::new();
fig.axes(0, 0).plot([0.0, 1.0, 2.0], [0.0]);
assert!(matches!(fig.show(), Err(Error::Invalid(_))));
}
#[test]
fn error_is_thread_safe_and_boxable() {
fn assert_thread_safe<E: std::error::Error + Send + Sync + 'static>() {}
assert_thread_safe::<Error>();
}