Skip to content

Commit 5d1562b

Browse files
Copilotszmyty
andcommitted
Cache transform output to avoid redundant execution across formats
Co-authored-by: szmyty <14865041+szmyty@users.noreply.github.com> Agent-Logs-Url: https://github.com/egohygiene/renderflow/sessions/137e1f01-2284-4261-80b5-48461c6b1012
1 parent 8e1169b commit 5d1562b

1 file changed

Lines changed: 64 additions & 20 deletions

File tree

src/commands/build.rs

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,39 +76,33 @@ pub fn run(config_path: &str, dry_run: bool) -> Result<()> {
7676
}
7777
info!("Selected outputs: {}", output_formats.join(", "));
7878

79-
// Two ticks per output format: one for transforms, one for rendering.
80-
let total_steps = config.outputs.len() as u64 * 2;
79+
// One tick for transforms (run once) plus one tick per output format for rendering.
80+
let total_steps = 1 + config.outputs.len() as u64;
8181
let pb = ProgressBar::new(total_steps);
8282
pb.set_style(
8383
ProgressStyle::with_template("{spinner:.cyan} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
8484
.expect("hardcoded progress bar template is valid")
8585
.progress_chars("█▓░"),
8686
);
8787

88+
// Transforms are pure in-memory operations (no files, no external commands) and
89+
// are not output-format dependent, so they are executed once and reused for all
90+
// output formats.
91+
let registry: TransformRegistry = register_transforms(&config.variables);
92+
info!("Applying transforms (cached for all outputs)");
93+
pb.set_message("Applying transforms");
94+
let transformed = registry
95+
.apply_all(normalized_content)
96+
.with_context(|| "Transform pipeline failed; no output formats will be rendered")?;
97+
pb.inc(1);
98+
8899
let mut failed_outputs: Vec<(String, anyhow::Error)> = Vec::new();
89100

90101
for output in &config.outputs {
91102
let format = output.output_type.clone();
92103
let output_path = format!("{}/{}.{}", output_dir.display(), input_stem, format);
93104
info!(format = %format, output = %output_path, template = ?output.template, "Running pipeline for format");
94105

95-
let registry: TransformRegistry = register_transforms(&config.variables);
96-
97-
// Transforms are pure in-memory operations (no files, no external commands),
98-
// so they run in both normal and dry-run mode to give an accurate preview.
99-
pb.set_message(format!("[{format}] Applying transforms"));
100-
let transformed = match registry.apply_all(normalized_content.clone()) {
101-
Ok(t) => t,
102-
Err(e) => {
103-
warn!(format = %format, error = %e, "Transform failed for output format");
104-
failed_outputs.push((format.to_string(), e));
105-
// Consume both the transform tick and the render tick we're skipping.
106-
pb.inc(2);
107-
continue;
108-
}
109-
};
110-
pb.inc(1);
111-
112106
if dry_run {
113107
info!("[dry-run] Would render {} output to: {}", format, output_path);
114108
pb.set_message(format!("[{format}] Would render output"));
@@ -120,7 +114,7 @@ pub fn run(config_path: &str, dry_run: bool) -> Result<()> {
120114
pipeline.add_step(Box::new(StrategyStep::new(strategy, &output_path)));
121115

122116
pb.set_message(format!("[{format}] Rendering output"));
123-
match pipeline.run_steps(transformed) {
117+
match pipeline.run_steps(transformed.clone()) {
124118
Ok(_) => {
125119
pb.inc(1);
126120
pb.println(format!("✔ Output written to: {}", output_path));
@@ -279,4 +273,54 @@ mod tests {
279273
let result = run("/nonexistent/renderflow.yaml", true);
280274
assert!(result.is_err(), "dry-run with missing config should still error");
281275
}
276+
277+
/// Build a config with multiple output formats for testing that transforms run once.
278+
fn multi_output_config_file() -> (NamedTempFile, tempfile::TempDir) {
279+
let dir = tempfile::tempdir().expect("failed to create temp dir");
280+
let input_path = dir.path().join("input.md");
281+
// Content includes emoji and a variable so transforms have real work to do
282+
// across both the EmojiTransform and VariableSubstitutionTransform stages.
283+
fs::write(&input_path, "# Hello 😀\n\nValue: {{greeting}}\n")
284+
.expect("failed to write input file");
285+
let output_dir = dir.path().join("dist");
286+
let config_content = format!(
287+
"outputs:\n - type: html\n - type: pdf\ninput: \"{}\"\noutput_dir: \"{}\"\nvariables:\n greeting: world\n",
288+
input_path.display(),
289+
output_dir.display()
290+
);
291+
let mut f = NamedTempFile::new().expect("failed to create temp file");
292+
f.write_all(config_content.as_bytes())
293+
.expect("failed to write temp file");
294+
(f, dir)
295+
}
296+
297+
#[test]
298+
fn test_dry_run_multiple_outputs_succeeds() {
299+
// Dry-run should succeed for multiple output formats without requiring
300+
// any external tools (pandoc/tectonic). Transforms run once and the
301+
// result is reused for each format.
302+
let (f, dir) = multi_output_config_file();
303+
let output_dir = dir.path().join("dist");
304+
let result = run(f.path().to_str().unwrap(), true);
305+
assert!(result.is_ok(), "dry-run with multiple outputs should succeed: {:?}", result);
306+
// No output directory should have been created in dry-run mode.
307+
assert!(!output_dir.exists(), "output directory must not be created in dry-run mode");
308+
}
309+
310+
#[test]
311+
fn test_transforms_applied_once_content_consistent_across_formats() {
312+
// Verify that transform output is consistent when multiple formats are
313+
// configured: the same variable substitution result should appear
314+
// regardless of how many output formats are requested. We exercise
315+
// this indirectly by checking that a dry-run with multiple outputs
316+
// succeeds with the same result as a single-output dry-run.
317+
let (single_f, _single_dir) = valid_config_file();
318+
let (multi_f, _multi_dir) = multi_output_config_file();
319+
320+
let single_result = run(single_f.path().to_str().unwrap(), true);
321+
let multi_result = run(multi_f.path().to_str().unwrap(), true);
322+
323+
assert!(single_result.is_ok(), "single-output dry-run failed: {:?}", single_result);
324+
assert!(multi_result.is_ok(), "multi-output dry-run failed: {:?}", multi_result);
325+
}
282326
}

0 commit comments

Comments
 (0)