mod common;
use std::path::PathBuf;
use common::image_figure;
use ironlab::ir::IssueKind;
use ironlab::prelude::*;
use ironlab::{
DepthPolicy, ExportReport, ExportWarning, ExportWarningKind, SceneWarning, UnverifiedCause,
};
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_then_load_round_trips_a_figure_with_every_image_kind_in_both_formats() {
let fig = image_figure();
let report = fig.validate();
assert!(report.is_valid(), "{report:?}");
for name in ["images.fig", "images.fig.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");
}
fn figure_with_an_undrawn_surface() -> (Figure, NodeId) {
let x = linspace(0.0, 3.0, 4);
let mut fig = Figure::new();
fig.axes(0, 0).plot(&x, &x);
let surface = fig
.axes(0, 0)
.surface(&x, [0.0], &Matrix::from_fn(1, 4, |_, col| col as f64))
.id();
(fig, surface)
}
#[test]
fn export_pdf_reports_the_artists_left_off_the_page_and_still_writes_the_file() {
let path = fresh("undrawn_surface.pdf");
let (fig, surface) = figure_with_an_undrawn_surface();
let report: ExportReport = fig.export_pdf(&path).unwrap();
let kinds: Vec<(IssueKind, Option<NodeId>)> = report
.validation
.iter()
.map(|issue| (issue.kind, issue.node))
.collect();
assert_eq!(
kinds,
vec![(IssueKind::NothingToDraw, Some(surface))],
"the validation warnings name the surface and nothing else: {:?}",
report.validation
);
let named: Vec<&SceneWarning> = report
.scene
.iter()
.filter(|warning| warning.node == Some(surface))
.collect();
assert_eq!(
named.len(),
1,
"one compiler warning names the surface: {:?}",
report.scene
);
assert!(
report
.scene
.iter()
.all(|warning| warning.node == Some(surface)),
"no compiler warning concerns anything else: {:?}",
report.scene
);
let bytes = std::fs::read(&path).unwrap();
assert!(bytes.starts_with(b"%PDF"), "the page was written");
}
#[test]
fn export_pdf_of_a_figure_with_nothing_left_out_returns_an_empty_report() {
let path = fresh("complete.pdf");
let report = sample_figure().export_pdf(&path).unwrap();
assert_eq!(report.validation, vec![], "{report:?}");
assert_eq!(report.scene, vec![], "{report:?}");
assert!(path.exists(), "the page was written");
}
#[test]
fn export_pdf_with_returns_the_same_report_as_export_pdf() {
let (fig, surface) = figure_with_an_undrawn_surface();
let plain_path = fresh("undrawn_plain.pdf");
let chosen_path = fresh("undrawn_chosen.pdf");
let plain = fig.export_pdf(&plain_path).unwrap();
let chosen = fig
.export_pdf_with(
&chosen_path,
RasterOptions {
policy: RasterPolicy::Never,
..RasterOptions::default()
},
)
.unwrap();
assert_eq!(chosen.validation, plain.validation);
assert_eq!(chosen.scene, plain.scene);
assert!(
chosen
.validation
.iter()
.any(|issue| issue.node == Some(surface)),
"{chosen:?}"
);
assert!(
plain_path.exists() && chosen_path.exists(),
"both pages were written"
);
}
fn two_dimensional_figure() -> Figure {
let x = linspace(0.0, 1.0, 5);
let mut fig = Figure::new();
fig.axes(0, 0).plot(&x, &x);
fig
}
fn three_dimensional_figure() -> (Figure, NodeId) {
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();
fig.axes(0, 0).surf(&x, &x, &z);
let axes = fig.axes(0, 0).id();
(fig, axes)
}
fn reported(warnings: &[ExportWarning]) -> Vec<(Option<NodeId>, ExportWarningKind)> {
warnings
.iter()
.map(|warning| (warning.node, warning.kind.clone()))
.collect()
}
fn vector_depth() -> RasterOptions {
RasterOptions {
depth: DepthPolicy::Vector,
..RasterOptions::default()
}
}
#[test]
fn export_pdf_of_a_two_dimensional_figure_reports_nothing_from_the_exporter() {
let path = fresh("two_dimensional.pdf");
let report = two_dimensional_figure().export_pdf(&path).unwrap();
assert!(report.export.is_empty(), "{report:?}");
assert!(
report.validation.is_empty() && report.scene.is_empty(),
"{report:?}"
);
assert!(path.exists(), "the page was written");
}
#[test]
fn export_pdf_with_the_vector_depth_policy_reports_the_three_dimensional_axes_as_unverified() {
let path = fresh("three_dimensional_vector.pdf");
let (fig, axes) = three_dimensional_figure();
let report = fig.export_pdf_with(&path, vector_depth()).unwrap();
assert_eq!(
reported(&report.export),
vec![(
Some(axes),
ExportWarningKind::Unverified {
cause: UnverifiedCause::PolicyVector,
},
)],
"the exporter reports the axes it left unchecked, and nothing else: {report:?}"
);
assert!(
!report.export[0].message.trim().is_empty(),
"the warning carries a message: {report:?}"
);
assert!(
report.validation.is_empty() && report.scene.is_empty(),
"nothing was left off the page: {report:?}"
);
let bytes = std::fs::read(&path).unwrap();
assert!(bytes.starts_with(b"%PDF"), "the page was written");
}
#[test]
fn export_pdf_with_the_vector_depth_policy_reports_each_three_dimensional_axes_once() {
let path = fresh("two_three_dimensional_axes.pdf");
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);
fig.axes(0, 0).surf(&x, &x, &z);
fig.axes(0, 1).mesh(&x, &x, &z);
let (left, right) = (fig.axes(0, 0).id(), fig.axes(0, 1).id());
assert_ne!(left, right, "the two axes are distinct nodes");
let report = fig.export_pdf_with(&path, vector_depth()).unwrap();
let mut nodes: Vec<Option<NodeId>> = report.export.iter().map(|w| w.node).collect();
nodes.sort();
let mut expected = vec![Some(left), Some(right)];
expected.sort();
assert_eq!(nodes, expected, "one warning per axes: {report:?}");
assert!(
report.export.iter().all(|warning| {
warning.kind
== ExportWarningKind::Unverified {
cause: UnverifiedCause::PolicyVector,
}
}),
"every warning gives the user's choice as the reason: {report:?}"
);
assert!(path.exists(), "the page was written");
}
#[test]
fn export_pdf_of_a_three_dimensional_figure_reports_only_what_the_machine_could_verify() {
let path = fresh("three_dimensional_auto.pdf");
let (fig, axes) = three_dimensional_figure();
let report = fig.export_pdf(&path).unwrap();
assert!(
report.export.len() <= 1,
"the one axes is reported at most once: {report:?}"
);
for warning in &report.export {
assert_eq!(
warning.node,
Some(axes),
"every warning names the axes: {report:?}"
);
assert!(
matches!(
warning.kind,
ExportWarningKind::Unverified {
cause: UnverifiedCause::NoAdapter
} | ExportWarningKind::RasterisedForDepth
),
"the exporter either had no adapter to verify the axes or found its order wrong: {warning:?}"
);
assert!(
!warning.message.trim().is_empty(),
"the warning carries a message: {warning:?}"
);
}
assert!(
report.validation.is_empty() && report.scene.is_empty(),
"nothing was left off the page: {report:?}"
);
let bytes = std::fs::read(&path).unwrap();
assert!(
bytes.starts_with(b"%PDF"),
"the page was written whatever the machine could verify"
);
}
#[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(), "no partial file is left behind");
}
#[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>();
}