mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 05:02:51 +00:00
Report unsupported files in recorded Batch folders
This commit is contained in:
1 parent
f3e5c63383
commit
60dba86c22
7 files changed
+120
-42
No files matched your search
@@ -118,7 +118,7 @@ fn read(d: &Descriptor) -> Option<Value> {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let mut p = json!({"input":path_text(d.get("null")?)?,"output":path_text(d.get("T ")?)?,"flatOutput":true,"stopOnError":true});
|
||||
let mut p = json!({"input":path_text(d.get("null")?)?,"output":path_text(d.get("T ")?)?,"flatOutput":true,"stopOnError":true,"includeNonImages":true});
|
||||
let D::Reference(items) = d.get("Usng")? else { return None };
|
||||
match items.as_slice() {
|
||||
[ReferenceItem::Name { class: action, name }, ReferenceItem::Name { class: set, name: set_name }]
|
||||
@@ -167,6 +167,7 @@ pub(super) fn batch_to(_: &str, p: &Value) -> Option<Descriptor> {
|
||||
"includeSubfolders",
|
||||
"overrideSaveAs",
|
||||
"stopOnError",
|
||||
"includeNonImages",
|
||||
"namingComponents",
|
||||
"startingSerial",
|
||||
"compatibility",
|
||||
@@ -176,8 +177,8 @@ pub(super) fn batch_to(_: &str, p: &Value) -> Option<Descriptor> {
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
// Without Log, the recorded dialog stops at the first failing file. Native defaults continue.
|
||||
if p.get("stopOnError") != Some(&Value::Bool(true)) {
|
||||
// Recorded folders include unsupported files and stop at the first error without Log.
|
||||
if p.get("stopOnError") != Some(&Value::Bool(true)) || p.get("includeNonImages") != Some(&Value::Bool(true)) {
|
||||
return None;
|
||||
}
|
||||
if let Some(original) = p.get(ORIGINAL) {
|
||||
|
||||
@@ -107,7 +107,7 @@ fn batch_descriptor(component: &str) -> Descriptor {
|
||||
|
||||
/// Preserve the recorded template in the generic registry round-trip witness.
|
||||
fn batch_case() -> Value {
|
||||
json!({"input":"/input","output":"/output","action":"Child","set":"S","flatOutput":true,"includeSubfolders":false,"overrideSaveAs":false,"stopOnError":true,
|
||||
json!({"input":"/input","output":"/output","action":"Child","set":"S","flatOutput":true,"includeSubfolders":false,"overrideSaveAs":false,"stopOnError":true,"includeNonImages":true,
|
||||
"namingComponents":[{"component":"serial3"}],"startingSerial":7,"compatibility":{"macintosh":true,"windows":false,"unix":true},
|
||||
"__atnBatch":b64(&batch_descriptor("threeDigit").to_bytes())})
|
||||
}
|
||||
@@ -124,6 +124,7 @@ fn batch_all_documented_components_and_compatibility_flags_round_trip() {
|
||||
assert_eq!(p["namingComponents"], json!([{"component":native}]));
|
||||
assert_eq!(p["startingSerial"], 7);
|
||||
assert_eq!(p["stopOnError"], true);
|
||||
assert_eq!(p["includeNonImages"], true);
|
||||
assert_eq!(p["compatibility"], json!({"macintosh":true,"windows":false,"unix":true}));
|
||||
let (out, skipped) = export("S", &[&imported.actions[0]], &[], false).unwrap();
|
||||
assert!(skipped.is_empty());
|
||||
@@ -151,14 +152,16 @@ fn batch_unknown_components_wrong_start_placement_and_unsupported_modes_stay_who
|
||||
let mut p = batch_case();
|
||||
p["target"] = json!("mask");
|
||||
assert!(super::steps_batch::batch_to("file.automate.batch", &p).is_none());
|
||||
for value in [Value::Null, json!(false), json!("yes")] {
|
||||
for key in ["stopOnError", "includeNonImages"] {
|
||||
for value in [Value::Null, json!(false), json!("yes")] {
|
||||
let mut p = batch_case();
|
||||
p[key] = value;
|
||||
assert!(super::steps_batch::batch_to("file.automate.batch", &p).is_none());
|
||||
}
|
||||
let mut p = batch_case();
|
||||
p["stopOnError"] = value;
|
||||
p.as_object_mut().unwrap().remove(key);
|
||||
assert!(super::steps_batch::batch_to("file.automate.batch", &p).is_none());
|
||||
}
|
||||
let mut p = batch_case();
|
||||
p.as_object_mut().unwrap().remove("stopOnError");
|
||||
assert!(super::steps_batch::batch_to("file.automate.batch", &p).is_none());
|
||||
}
|
||||
|
||||
/// A set with a mapped filter, an adjustment layer, selection steps, a layer by name, a Stop,
|
||||
|
||||
@@ -31,7 +31,7 @@ mod save;
|
||||
pub use batch_run::OutputClaims;
|
||||
pub(crate) use batch_run::{BatchInput, batch_action, batch_inputs, batch_keeps_step, batch_save_point, process_files};
|
||||
#[cfg(test)]
|
||||
pub(crate) use batch_run::{MAX_SUBFOLDER_DEPTH, list_images_tree};
|
||||
pub(crate) use batch_run::{MAX_SUBFOLDER_DEPTH, list_files_tree};
|
||||
use batch_run::{batch, same_format};
|
||||
pub use export::{LayerExportReport, SkippedExportLayer, bake_cube, export_layers_report_with, export_layers_with};
|
||||
use export::{color_lookup_tables, layers_to_files, push_unique};
|
||||
@@ -308,7 +308,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Batch…",
|
||||
&["File", "Automate"],
|
||||
None,
|
||||
r##"{"action":name|index? (instead of steps),"set":str?,"steps":[[commandId,params]|{"command":id,"params":{}}…] (a recorded action; a Save, Save As or Save a Copy step marks the document written to output, in this command's format and options: as the save found it if it was closed or another document is active at the end, else as the action leaves it; with no such step, the active document at the end),"input":folder|[paths],"includeSubfolders":bool=false (a folder input also takes the images in its subfolders, up to 32 levels and 100000 files; each result keeps its subfolder under output),"output":folder,"format":"same|png|jpg|psd|tiff|…"="same","quality":0..12?,"jpegFormat":"baseline|optimized|progressive"?,"namingComponents":[{"component":choice}|{"text":str}]? (up to six; the exact filename, with no automatic extension),"compatibility":{"macintosh":bool,"windows":bool,"unix":bool}?,"flatOutput":bool=false (structured naming only),"overrideSaveAs":bool? (recorded routing; requires a directly represented Save configuration),"stopOnError":bool=false (report the first failed input and stop; earlier outputs remain saved),"naming":string? (blank keeps source names; {name}, {serial} padded to 3 digits, {date} as YYYY-MM-DD UTC; extension added automatically),"startingSerial":1..4294967295=1 (input position, including failed inputs),"tiffLayers":bool=false,"tiffCompression":"none|lzw|zip"?,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"calledActions":[{"name","steps"}…]? (the actions Play Action steps call; default: the session's)} → {files, errors} (an input whose output name was already written in the run goes to errors)"##,
|
||||
r##"{"action":name|index? (instead of steps),"set":str?,"steps":[[commandId,params]|{"command":id,"params":{}}…] (a recorded action; a Save, Save As or Save a Copy step marks the document written to output, in this command's format and options: as the save found it if it was closed or another document is active at the end, else as the action leaves it; with no such step, the active document at the end),"input":folder|[paths],"includeNonImages":bool=false (folder inputs report unsupported files as per-file errors instead of skipping them; arrays are unchanged),"includeSubfolders":bool=false (a folder input also takes the images in its subfolders, up to 32 levels and 100000 files; each result keeps its subfolder under output),"output":folder,"format":"same|png|jpg|psd|tiff|…"="same","quality":0..12?,"jpegFormat":"baseline|optimized|progressive"?,"namingComponents":[{"component":choice}|{"text":str}]? (up to six; the exact filename, with no automatic extension),"compatibility":{"macintosh":bool,"windows":bool,"unix":bool}?,"flatOutput":bool=false (structured naming only),"overrideSaveAs":bool? (recorded routing; requires a directly represented Save configuration),"stopOnError":bool=false (report the first failed input and stop; earlier outputs remain saved),"naming":string? (blank keeps source names; {name}, {serial} padded to 3 digits, {date} as YYYY-MM-DD UTC; extension added automatically),"startingSerial":1..4294967295=1 (input position, including failed inputs),"tiffLayers":bool=false,"tiffCompression":"none|lzw|zip"?,"tgaBits":24|32?,"dpxBits":8|10|12|16? (DPX: bits per sample; unset, 8 for an 8-bit document, else 16),"ddsCompression":"none|bc1|bc2|bc3"="none" (DDS: uncompressed, or block compression; BC1 keeps alpha on or off),"ddsMipmaps":bool=false (DDS: also write the full mip chain),"calledActions":[{"name","steps"}…]? (the actions Play Action steps call; default: the session's)} → {files, errors} (an input whose output name was already written in the run goes to errors)"##,
|
||||
native,
|
||||
batch
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::open::import_at_raw_ppi;
|
||||
use super::paths::{extension, file_name, join, list_images, read_file, stem};
|
||||
use super::paths::{extension, file_name, join, list_files, list_images, read_file, stem};
|
||||
use super::processing::parse_steps;
|
||||
use super::save::{SaveOpts, save_doc};
|
||||
use super::{Arc, Document, EngineError, Result, Session, Value, batch_naming, json, str_param};
|
||||
@@ -33,23 +33,31 @@ pub(crate) const MAX_SUBFOLDER_FILES: usize = 100_000;
|
||||
/// subfolders, each tagged with its relative folder. `output` (when inside the input folder, as
|
||||
/// the dialog's default `<input>/batch` is) is not walked, so a rerun doesn't process its results.
|
||||
pub(crate) fn batch_inputs_tree(p: &Value, cmd: &str, output: &str) -> Result<Vec<BatchInput>> {
|
||||
collect_inputs(p, cmd, output, false)
|
||||
}
|
||||
|
||||
/// Collect folder inputs with Batch's explicit unsupported-file policy; arrays keep their order.
|
||||
fn collect_inputs(p: &Value, cmd: &str, output: &str, include_non_images: bool) -> Result<Vec<BatchInput>> {
|
||||
let recurse = match p.get("includeSubfolders") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(_) => return Err(EngineError::BadParams { cmd: cmd.into(), msg: "\"includeSubfolders\" must be true or false".into() }),
|
||||
};
|
||||
match p.get("input").or_else(|| p.get("files")) {
|
||||
Some(Value::String(dir)) if recurse => list_images_tree(dir, output, cmd),
|
||||
Some(Value::String(dir)) if recurse => list_files_tree(dir, output, cmd, include_non_images),
|
||||
Some(Value::String(dir)) if include_non_images => {
|
||||
Ok(list_files(dir, true)?.into_iter().map(|path| BatchInput { path, rel_dir: String::new() }).collect())
|
||||
}
|
||||
_ => Ok(batch_inputs(p, cmd)?.into_iter().map(|path| BatchInput { path, rel_dir: String::new() }).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The images under `dir` and its subfolders: each folder's files sorted by name, then its
|
||||
/// The selected files under `dir` and its subfolders: each folder's files sorted by name, then its
|
||||
/// subfolders in name order (depth first). Bounded by [`MAX_SUBFOLDER_DEPTH`] and
|
||||
/// [`MAX_SUBFOLDER_FILES`]; a folder reached twice (a symlink loop) is walked once, and `skip`
|
||||
/// (the output folder) is left out.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn list_images_tree(dir: &str, skip: &str, cmd: &str) -> Result<Vec<BatchInput>> {
|
||||
pub(crate) fn list_files_tree(dir: &str, skip: &str, cmd: &str, include_non_images: bool) -> Result<Vec<BatchInput>> {
|
||||
use std::path::{Path, PathBuf};
|
||||
let canon = |p: &Path| std::fs::canonicalize(p).ok();
|
||||
let root = Path::new(dir);
|
||||
@@ -63,13 +71,13 @@ pub(crate) fn list_images_tree(dir: &str, skip: &str, cmd: &str) -> Result<Vec<B
|
||||
while let Some((folder, rel, depth)) = stack.pop() {
|
||||
let shown = folder.to_string_lossy().into_owned();
|
||||
// The top folder must be readable; a subfolder that isn't is skipped.
|
||||
let files = match list_images(&shown) {
|
||||
let files = match list_files(&shown, include_non_images) {
|
||||
Ok(f) => f,
|
||||
Err(e) if depth == 0 => return Err(e),
|
||||
Err(_) => continue,
|
||||
};
|
||||
if out.len().saturating_add(files.len()) > MAX_SUBFOLDER_FILES {
|
||||
return Err(EngineError::BadParams { cmd: cmd.into(), msg: format!("{dir} holds more than {MAX_SUBFOLDER_FILES} images with its subfolders") });
|
||||
return Err(EngineError::BadParams { cmd: cmd.into(), msg: format!("{dir} holds more than {MAX_SUBFOLDER_FILES} files with its subfolders") });
|
||||
}
|
||||
out.extend(files.into_iter().map(|path| BatchInput { path, rel_dir: rel.clone() }));
|
||||
if depth >= MAX_SUBFOLDER_DEPTH {
|
||||
@@ -97,7 +105,7 @@ pub(crate) fn list_images_tree(dir: &str, skip: &str, cmd: &str) -> Result<Vec<B
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) fn list_images_tree(dir: &str, _skip: &str, _cmd: &str) -> Result<Vec<BatchInput>> {
|
||||
pub(crate) fn list_files_tree(dir: &str, _skip: &str, _cmd: &str, _include_non_images: bool) -> Result<Vec<BatchInput>> {
|
||||
Err(EngineError::Other(format!("cannot list {dir}: no file system on the web")))
|
||||
}
|
||||
|
||||
@@ -129,7 +137,7 @@ pub(crate) fn process_tree(
|
||||
suffix: &str,
|
||||
f: &dyn Fn(&mut Session, &str) -> Result<bool>,
|
||||
) -> Value {
|
||||
process_outputs(s, default_outputs(inputs, output, format, suffix), save, None, false, f)
|
||||
process_outputs(s, default_outputs(inputs, output, format, suffix), save, None, false, false, f)
|
||||
}
|
||||
|
||||
/// Stream legacy destination names without collecting another path list for large batches.
|
||||
@@ -188,6 +196,7 @@ fn process_outputs<'a>(
|
||||
save: SaveOpts,
|
||||
content_format: Option<&str>,
|
||||
stop_on_error: bool,
|
||||
check_folder_format: bool,
|
||||
f: &dyn Fn(&mut Session, &str) -> Result<bool>,
|
||||
) -> Value {
|
||||
// The scratch sessions have default preferences; a raw opens at the user's Raw Defaults.
|
||||
@@ -198,6 +207,13 @@ fn process_outputs<'a>(
|
||||
for (path, out) in outputs {
|
||||
let r = (|| -> Result<()> {
|
||||
written.check(&out).map_err(EngineError::Other)?;
|
||||
// A folder must not silently use the embedded preview of an unsupported raw format.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if check_folder_format && !super::paths::openable_name(path) {
|
||||
return Err(EngineError::Other(format!("unsupported Batch folder input: {path}")));
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = check_folder_format;
|
||||
let bytes = read_file(path)?;
|
||||
let mut scratch = Session::new();
|
||||
let doc = import_at_raw_ppi(raw_ppi, &file_name(path), &bytes)?;
|
||||
@@ -286,6 +302,11 @@ pub(crate) fn batch_action(list: &[crate::actions_cmds::Action], p: &Value) -> R
|
||||
/// Apply the selected file-error policy while retaining per-file results for direct callers.
|
||||
pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "file.automate.batch";
|
||||
let include_non_images = match p.get("includeNonImages") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(value)) => *value,
|
||||
_ => return Err(EngineError::BadParams { cmd: cmd.into(), msg: "includeNonImages must be a boolean".into() }),
|
||||
};
|
||||
let stop_on_error = match p.get("stopOnError") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(value)) => *value,
|
||||
@@ -338,7 +359,8 @@ pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(EngineError::BadParams { cmd: cmd.into(), msg: format!("unknown command `{id}` in the action") });
|
||||
}
|
||||
let output = str_param(p, "output", cmd)?.to_string();
|
||||
let inputs = batch_inputs_tree(p, cmd, &output)?;
|
||||
let inputs = collect_inputs(p, cmd, &output, include_non_images)?;
|
||||
let check_folder_format = include_non_images && p.get("input").or_else(|| p.get("files")).is_some_and(Value::is_string);
|
||||
let format = p.get("format").and_then(Value::as_str).unwrap_or("same").to_string();
|
||||
// A batch inside a played action charges its inputs × steps (at least one each: an input is read,
|
||||
// decoded and saved even with no steps) to the playback tree it is in and
|
||||
@@ -405,8 +427,16 @@ pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
result
|
||||
};
|
||||
let r = match names {
|
||||
Some(names) => process_outputs(s, inputs.iter().map(|i| i.path.as_str()).zip(names), save, structured.then_some(format.as_str()), stop_on_error, &run),
|
||||
None => process_outputs(s, default_outputs(&inputs, &output, &format, ""), save, None, stop_on_error, &run),
|
||||
Some(names) => process_outputs(
|
||||
s,
|
||||
inputs.iter().map(|i| i.path.as_str()).zip(names),
|
||||
save,
|
||||
structured.then_some(format.as_str()),
|
||||
stop_on_error,
|
||||
check_folder_format,
|
||||
&run,
|
||||
),
|
||||
None => process_outputs(s, default_outputs(&inputs, &output, &format, ""), save, None, stop_on_error, check_folder_format, &run),
|
||||
};
|
||||
if s.actions.playing > 0 {
|
||||
s.actions.playback_steps = carried.get();
|
||||
|
||||
@@ -37,13 +37,18 @@ pub(crate) fn write_file(path: &str, _bytes: &[u8]) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Files of a folder that look like images we can open, sorted by name.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn list_images(dir: &str) -> Result<Vec<String>> {
|
||||
list_files(dir, false)
|
||||
}
|
||||
|
||||
/// Sorted regular files, optionally retaining unsupported names for per-file Batch errors.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn list_files(dir: &str, include_non_images: bool) -> Result<Vec<String>> {
|
||||
let rd = std::fs::read_dir(dir).map_err(|e| EngineError::Other(format!("{dir}: {e}")))?;
|
||||
let mut out: Vec<String> = rd
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.is_file() && openable_name(&p.to_string_lossy()))
|
||||
.filter(|p| p.is_file() && (include_non_images || openable_name(&p.to_string_lossy())))
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.collect();
|
||||
out.sort();
|
||||
@@ -51,7 +56,7 @@ pub fn list_images(dir: &str) -> Result<Vec<String>> {
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn list_images(dir: &str) -> Result<Vec<String>> {
|
||||
pub(super) fn list_files(dir: &str, _include_non_images: bool) -> Result<Vec<String>> {
|
||||
Err(EngineError::Other(format!("cannot list {dir}: no file system on the web")))
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ fn batch_structured_names_keep_image_format_and_refuse_unsupported_save_routing(
|
||||
child.set = Some("S".into());
|
||||
s.actions.list.push(child);
|
||||
let params = json!({"input":dir,"output":output,"action":"Child","set":"S","includeSubfolders":false,"flatOutput":true,
|
||||
"overrideSaveAs":false,"stopOnError":true,"namingComponents":[{"text":"from-atn"}]});
|
||||
"overrideSaveAs":false,"stopOnError":true,"includeNonImages":true,"namingComponents":[{"text":"from-atn"}]});
|
||||
let driver = crate::actions_cmds::Action::new("Driver", vec![("file.automate.batch".into(), params)]);
|
||||
let (bytes, skipped) = crate::atn_cmds::export("S", &[&driver], &[], false).unwrap();
|
||||
assert!(skipped.is_empty());
|
||||
@@ -84,10 +84,12 @@ fn batch_structured_names_keep_image_format_and_refuse_unsupported_save_routing(
|
||||
#[test]
|
||||
fn batch_recorded_stop_for_errors_retains_earlier_outputs_and_stops_before_later_inputs() {
|
||||
let dir = tmp("batch-stop-for-errors");
|
||||
let first = png(&dir, "a.png", 3, 2, "#ff0000");
|
||||
let bad = join(&dir, "b.png");
|
||||
std::fs::write(&bad, b"not a PNG").unwrap();
|
||||
let last = png(&dir, "c.png", 3, 2, "#0000ff");
|
||||
let input = join(&dir, "in");
|
||||
std::fs::create_dir_all(&input).unwrap();
|
||||
let first = png(&input, "a.png", 3, 2, "#ff0000");
|
||||
let bad = join(&input, "b.txt");
|
||||
std::fs::write(&bad, b"not an image").unwrap();
|
||||
let last = png(&input, "c.png", 3, 2, "#0000ff");
|
||||
let mut s = Session::new();
|
||||
for (mode, stop) in [("default", None), ("continue", Some(false)), ("stop", Some(true))] {
|
||||
let output = join(&dir, mode);
|
||||
@@ -102,11 +104,15 @@ fn batch_recorded_stop_for_errors_retains_earlier_outputs_and_stops_before_later
|
||||
assert!(std::path::Path::new(&join(&output, "a.png")).is_file());
|
||||
assert_eq!(std::path::Path::new(&join(&output, "c.png")).exists(), stop != Some(true));
|
||||
}
|
||||
// Native folder defaults still select images only.
|
||||
let native = s.execute("file.automate.batch", json!({"input":input,"output":join(&dir,"folder-default"),"steps":[]})).unwrap();
|
||||
assert_eq!(native["files"].as_array().unwrap().len(), 2, "{native}");
|
||||
assert!(native["errors"].as_array().unwrap().is_empty(), "{native}");
|
||||
let mut child = crate::actions_cmds::Action::new("Child", vec![]);
|
||||
child.set = Some("S".into());
|
||||
s.actions.list.push(child);
|
||||
let output = join(&dir, "recorded");
|
||||
let p = json!({"input":dir,"output":output,"action":"Child","set":"S","stopOnError":true,
|
||||
let p = json!({"input":input,"output":output,"action":"Child","set":"S","stopOnError":true,"includeNonImages":true,
|
||||
"includeSubfolders":false,"overrideSaveAs":false,"flatOutput":true,
|
||||
"namingComponents":[{"component":"name"},{"component":"extension"}]});
|
||||
let sentinel = json!({"width":7,"height":9});
|
||||
@@ -119,7 +125,7 @@ fn batch_recorded_stop_for_errors_retains_earlier_outputs_and_stops_before_later
|
||||
let result = s.execute("actions.play", json!({"action":"Driver"})).unwrap();
|
||||
assert_eq!(result["ran"], 0, "{result}");
|
||||
assert_eq!(result["failed"]["id"], "file.automate.batch");
|
||||
assert!(result["failed"]["error"].as_str().unwrap().contains("b.png"), "{result}");
|
||||
assert!(result["failed"]["error"].as_str().unwrap().contains("b.txt"), "{result}");
|
||||
assert!(s.documents().is_empty(), "the command after the failed Batch must not run");
|
||||
assert!(std::path::Path::new(&join(&output, "a.png")).is_file());
|
||||
assert!(!std::path::Path::new(&join(&output, "c.png")).exists());
|
||||
@@ -128,10 +134,40 @@ fn batch_recorded_stop_for_errors_retains_earlier_outputs_and_stops_before_later
|
||||
assert_eq!(result["ok"], true, "{result}");
|
||||
assert_eq!(result["results"][0]["result"]["errors"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(s.documents().len(), 1, "the direct script caller still runs its next command");
|
||||
let invalid_output = join(&dir, "invalid");
|
||||
let err = s.execute("file.automate.batch", json!({"input":[first],"output":invalid_output,"steps":[],"stopOnError":"true"})).unwrap_err();
|
||||
assert!(err.to_string().contains("stopOnError"), "{err}");
|
||||
assert!(!std::path::Path::new(&invalid_output).exists());
|
||||
// The recursive mode keeps output exclusion and reports excluded RAW names before import.
|
||||
let output = join(&input, "out");
|
||||
std::fs::create_dir_all(&output).unwrap();
|
||||
png(&output, "ignored.png", 3, 2, "#ffffff");
|
||||
let sub = join(&input, "sub");
|
||||
std::fs::create_dir_all(&sub).unwrap();
|
||||
let raw = join(&sub, "d.cr3");
|
||||
std::fs::copy(&first, &raw).unwrap();
|
||||
let result = s
|
||||
.execute(
|
||||
"file.automate.batch",
|
||||
json!({"input":input,"output":output,"steps":[],"format":"png",
|
||||
"includeSubfolders":true,"includeNonImages":true}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result["files"], json!([join(&output, "a.png"), join(&output, "c.png")]), "{result}");
|
||||
assert_eq!(result["errors"].as_array().unwrap().len(), 2, "{result}");
|
||||
assert_eq!(result["errors"][0]["file"], bad);
|
||||
assert_eq!(result["errors"][1]["file"], raw);
|
||||
assert!(result["errors"][1]["error"].as_str().unwrap().contains("unsupported Batch folder input"), "{result}");
|
||||
assert!(!std::path::Path::new(&join(&output, "sub/d.png")).exists());
|
||||
// Explicit arrays still sniff the image content, including with the folder option enabled.
|
||||
let output = join(&dir, "explicit-array");
|
||||
let result = s.execute("file.automate.batch", json!({"input":[raw],"output":output,"steps":[],"format":"png","includeNonImages":true})).unwrap();
|
||||
assert_eq!(result["files"], json!([join(&output, "d.png")]), "{result}");
|
||||
assert!(result["errors"].as_array().unwrap().is_empty(), "{result}");
|
||||
for key in ["stopOnError", "includeNonImages"] {
|
||||
let invalid_output = join(&dir, key);
|
||||
let mut p = json!({"input":[first],"output":invalid_output,"steps":[]});
|
||||
p[key] = json!("true");
|
||||
let err = s.execute("file.automate.batch", p).unwrap_err();
|
||||
assert!(err.to_string().contains(key), "{err}");
|
||||
assert!(!std::path::Path::new(&invalid_output).exists());
|
||||
}
|
||||
}
|
||||
|
||||
fn tmp(name: &str) -> String {
|
||||
@@ -1796,7 +1832,7 @@ fn batch_subfolder_walk_is_depth_limited() {
|
||||
std::fs::write(join(&deep, &format!("l{level}.png")), b"x").unwrap();
|
||||
}
|
||||
}
|
||||
let found = list_images_tree(&dir, "", "test").unwrap();
|
||||
let found = list_files_tree(&dir, "", "test", false).unwrap();
|
||||
let names: Vec<_> = found.iter().map(|i| file_name(&i.path)).collect();
|
||||
assert_eq!(names, vec![format!("l{MAX_SUBFOLDER_DEPTH}.png")], "{found:?}");
|
||||
assert_eq!(found[0].rel_dir.split('/').count(), MAX_SUBFOLDER_DEPTH);
|
||||
@@ -1822,7 +1858,7 @@ fn batch_subfolder_walk_survives_symlink_loops() {
|
||||
eprintln!("skipped: cannot create a directory symlink here");
|
||||
return;
|
||||
}
|
||||
let found = list_images_tree(&dir, "", "test").unwrap();
|
||||
let found = list_files_tree(&dir, "", "test", false).unwrap();
|
||||
let names: Vec<_> = found.iter().map(|i| (i.rel_dir.as_str(), file_name(&i.path))).collect();
|
||||
assert_eq!(names, vec![("", "a.png".to_string()), ("sub", "b.png".to_string())]);
|
||||
}
|
||||
|
||||
+7
-4
@@ -254,10 +254,13 @@ The original native template format and subfolder layout remain the defaults;
|
||||
recorded folder steps use flat structured output. Duplicate output protection
|
||||
remains in place rather than silently overwriting an earlier result.
|
||||
|
||||
Recorded folder steps without an error log set `stopOnError`: the first failed
|
||||
input is reported, earlier outputs stay saved and later inputs are not processed.
|
||||
The native command still continues by default. That continuing mode cannot be
|
||||
exported as a no-log Batch step, which would change its error policy.
|
||||
Recorded folder steps set `includeNonImages`: ordinary non-image files and
|
||||
unsupported RAW extensions become per-file errors instead of being skipped or
|
||||
opened through an embedded preview. Native folder inputs still select images
|
||||
only by default; explicit file arrays are unchanged. Without an error log,
|
||||
recorded steps also set `stopOnError`: the first failed input is reported,
|
||||
earlier outputs stay saved and later inputs are not processed. Native Batch
|
||||
continues by default. Export requires both recorded policies to preserve them.
|
||||
Any reported file error fails a recorded Batch step, so its parent action does
|
||||
not run later steps. A direct scripting call still returns the collected file
|
||||
results successfully, as measured in the reference application.
|
||||
|
||||
Reference in new issue
Block a user