diff --git a/crates/engine/src/cmd/layout/tests.rs b/crates/engine/src/cmd/layout/tests.rs index 9bb73f5fc..2cb9b0c2e 100644 --- a/crates/engine/src/cmd/layout/tests.rs +++ b/crates/engine/src/cmd/layout/tests.rs @@ -303,3 +303,36 @@ fn mspace_edits_model_through_viewport() { s.execute("layout.set", &json!({"name": "Model"})).unwrap(); assert!(s.state().unwrap().mspace.is_none()); } + +/// The path's extension picks the plot format as EXPORT does, in any case; one that names no +/// format (or none) is refused instead of writing a PDF under that name, and a device that writes +/// another format is refused. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn plot_path_extension_picks_the_format_like_export() { + super::super::file::set_io(super::super::file::IoHooks { + read: |_, _| Err("no reader in tests".into()), + write: |_, _| Err("no writer in tests".into()), + plot: Some(fake_plot), + }); + if io().and_then(|h| h.plot).is_none() { + return; // another test installed hooks without a plotter first + } + let mut s = session_with_model(); + let dir = std::env::temp_dir().join(format!("cadcraft-plot-ext-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = |n: &str| dir.join(n).to_string_lossy().to_string(); + let upper = s.execute("plot", &json!({"path": path("X.PNG")})).unwrap(); + assert_eq!(upper["format"], "png", "an upper-case extension still picks png"); + let e = s.execute("plot", &json!({"path": path("x.txt")})).unwrap_err().to_string(); + assert!(e.contains("png") && e.contains("svg") && e.contains("pdf"), "{e}"); + assert!(!dir.join("x.txt").exists(), "nothing written under an unknown extension"); + let e = s.execute("plot", &json!({"path": path("noext")})).unwrap_err().to_string(); + assert!(e.contains("png") && e.contains("svg") && e.contains("pdf"), "{e}"); + assert!(!dir.join("noext").exists()); + let e = s.execute("plot", &json!({"path": path("y.png"), "device": "CADCraft PDF"})).unwrap_err().to_string(); + assert!(e.contains("writes pdf") && e.contains("png"), "{e}"); + // An explicit output still decides for an odd name. + assert_eq!(s.execute("plot", &json!({"path": path("z.txt"), "output": "pdf"})).unwrap()["format"], "pdf"); + std::fs::remove_dir_all(&dir).ok(); +} diff --git a/crates/engine/src/cmd/plotters.rs b/crates/engine/src/cmd/plotters.rs index 521b67cf9..d9aed574f 100644 --- a/crates/engine/src/cmd/plotters.rs +++ b/crates/engine/src/cmd/plotters.rs @@ -296,6 +296,19 @@ pub(crate) fn plot_options(s: &Session, p: &Value, cmd: &str) -> Result { // The path's extension names the format, like EXPORT, unless a device is given; a given // device that writes another format is refused. EXPORTPDF always writes PDF. let by_path = if cmd == "exportpdf" { None } else { str_param(p, "path").and_then(path_output) }; + // A path whose extension names no format (or has none) would hold a PDF under that name: + // refuse it, as EXPORT does, unless `output` or a device says what to write. + if cmd == "plot" + && by_path.is_none() + && p.get("output").is_none() + && !given.is_some_and(|n| !n.is_empty()) + && let Some(path) = str_param(p, "path") + { + return Err(bad( + cmd, + format!("can't tell the format of `{path}`: give a path ending in .png, .svg or .pdf, or set `output` to png, svg or pdf"), + )); + } if let (Some(want), Some(dev), true) = (by_path, &dev, given.is_some_and(|n| !n.is_empty())) && dev.output != want { diff --git a/crates/engine/tests/plot_format.rs b/crates/engine/tests/plot_format.rs index 1f4509c8c..48a4306bb 100644 --- a/crates/engine/tests/plot_format.rs +++ b/crates/engine/tests/plot_format.rs @@ -36,11 +36,12 @@ fn the_extension_picks_png_svg_or_pdf() { assert_eq!(r["format"], format, "{name}: the reply names the format"); assert_eq!(magic(&out), head, "{name}: the file holds that format"); } - // No path, or an extension that names no format: PDF as before. + // No path: PDF data as before. An extension that names no format is refused, like EXPORT. assert_eq!(s.execute("plot", &json!({ "layout": "Model" })).unwrap()["format"], "pdf"); let other = dir.join("e.plt"); - assert_eq!(s.execute("plot", &json!({ "path": other.to_string_lossy(), "layout": "Model" })).unwrap()["format"], "pdf"); - assert_eq!(magic(&other), b"%PDF-"); + let e = s.execute("plot", &json!({ "path": other.to_string_lossy(), "layout": "Model" })).unwrap_err().to_string(); + assert!(e.contains("png") && e.contains("svg") && e.contains("pdf"), "{e}"); + assert!(!other.exists(), "nothing is written under a name that is not a format"); // EXPORTPDF always writes PDF. let pdf = dir.join("f.png"); s.execute("exportpdf", &json!({ "path": pdf.to_string_lossy(), "layout": "Model" })).unwrap();