1use crate::error::{PathErrorExt, SsgError};
15use crate::walk;
16use std::{
17 fs,
18 path::{Path, PathBuf},
19};
20
21pub const DEFAULT_MEMORY_BUDGET_MB: usize = 512;
23
24const ESTIMATED_BYTES_PER_PAGE: usize = 64 * 1024; #[derive(Debug, Clone, Copy)]
30pub struct MemoryBudget {
31 pub max_bytes: usize,
33 pub batch_size: usize,
35}
36
37impl MemoryBudget {
38 #[must_use]
50 pub fn from_mb(mb: usize) -> Self {
51 let max_bytes = mb * 1024 * 1024;
52 let batch_size = (max_bytes / ESTIMATED_BYTES_PER_PAGE).max(10);
53 Self {
54 max_bytes,
55 batch_size,
56 }
57 }
58
59 #[must_use]
70 pub fn default_budget() -> Self {
71 Self::from_mb(DEFAULT_MEMORY_BUDGET_MB)
72 }
73}
74
75pub fn batched_content_files(
93 content_dir: &Path,
94 budget: &MemoryBudget,
95) -> Result<Vec<Vec<PathBuf>>, SsgError> {
96 let all_files = walk::walk_files(content_dir, "md")?;
97
98 if all_files.is_empty() {
99 return Ok(vec![]);
100 }
101
102 let batches: Vec<Vec<PathBuf>> = all_files
103 .chunks(budget.batch_size)
104 .map(|chunk| chunk.to_vec())
105 .collect();
106
107 log::info!(
108 "[streaming] {} file(s) in {} batch(es) (budget: {} MB, {} pages/batch)",
109 all_files.len(),
110 batches.len(),
111 budget.max_bytes / (1024 * 1024),
112 budget.batch_size,
113 );
114
115 Ok(batches)
116}
117
118pub fn compile_batch(
135 batch: &[PathBuf],
136 content_dir: &Path,
137 build_dir: &Path,
138 site_dir: &Path,
139 template_dir: &Path,
140 batch_idx: usize,
141) -> Result<(), SsgError> {
142 if batch.is_empty() {
143 return Ok(());
144 }
145
146 let batch_content = build_dir.join(format!(".batch-{batch_idx}"));
148 fs::create_dir_all(&batch_content).with_path(&batch_content)?;
149
150 for file in batch {
152 let rel = file.strip_prefix(content_dir).unwrap_or(file);
153 let dest = batch_content.join(rel);
154 if let Some(parent) = dest.parent() {
155 fs::create_dir_all(parent).with_path(parent)?;
156 }
157 let _ = fs::copy(file, &dest).with_path(&dest)?;
158 }
159
160 let batch_build = build_dir.join(format!(".batch-{batch_idx}-build"));
162 let batch_site = build_dir.join(format!(".batch-{batch_idx}-site"));
163 fs::create_dir_all(&batch_build).with_path(&batch_build)?;
164 fs::create_dir_all(&batch_site).with_path(&batch_site)?;
165
166 let compile_result = staticdatagen::compile(
167 &batch_build,
168 &batch_content,
169 &batch_site,
170 template_dir,
171 );
172
173 if compile_result.is_ok() {
175 fs::create_dir_all(site_dir).with_path(site_dir)?;
176 merge_dir(&batch_site, site_dir)?;
177 }
178
179 let _ = fs::remove_dir_all(&batch_content);
181 let _ = fs::remove_dir_all(&batch_build);
182 let _ = fs::remove_dir_all(&batch_site);
183
184 compile_result.map_err(|e| {
185 SsgError::io(
186 std::io::Error::other(format!("batch {batch_idx}: {e:?}")),
187 build_dir,
188 )
189 })
190}
191
192fn merge_dir(src: &Path, dst: &Path) -> Result<(), SsgError> {
194 if !src.exists() {
195 return Ok(());
196 }
197
198 for entry in fs::read_dir(src).with_path(src)? {
199 let entry = entry.with_path(src)?;
200 let path = entry.path();
201 let dest = dst.join(entry.file_name());
202
203 if path.is_dir() {
204 fs::create_dir_all(&dest).with_path(&dest)?;
205 merge_dir(&path, &dest)?;
206 } else {
207 let _ = fs::copy(&path, &dest).with_path(&dest)?;
208 }
209 }
210 Ok(())
211}
212
213#[must_use]
230pub fn should_stream(
231 content_dir: &Path,
232 budget: &MemoryBudget,
233 explicitly_set: bool,
234) -> bool {
235 if explicitly_set {
236 return true;
237 }
238
239 let count = walk::walk_files(content_dir, "md").map_or(0, |f| f.len());
240
241 count > budget.batch_size
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use tempfile::tempdir;
248
249 #[test]
250 fn memory_budget_from_mb() {
251 let budget = MemoryBudget::from_mb(256);
252 assert_eq!(budget.max_bytes, 256 * 1024 * 1024);
253 assert!(budget.batch_size > 0);
254 }
255
256 #[test]
257 fn memory_budget_default() {
258 let budget = MemoryBudget::default_budget();
259 assert_eq!(budget.max_bytes, 512 * 1024 * 1024);
260 }
261
262 #[test]
263 fn memory_budget_minimum_batch_size() {
264 let budget = MemoryBudget::from_mb(0);
265 assert!(
266 budget.batch_size >= 10,
267 "batch size should have a floor of 10"
268 );
269 }
270
271 #[test]
272 fn batched_content_files_empty_dir() {
273 let dir = tempdir().unwrap();
274 let content = dir.path().join("content");
275 fs::create_dir_all(&content).unwrap();
276
277 let budget = MemoryBudget::from_mb(512);
278 let batches = batched_content_files(&content, &budget).unwrap();
279 assert!(batches.is_empty());
280 }
281
282 #[test]
283 fn batched_content_files_splits_correctly() {
284 let dir = tempdir().unwrap();
285 let content = dir.path().join("content");
286 fs::create_dir_all(&content).unwrap();
287
288 for i in 0..25 {
289 fs::write(
290 content.join(format!("page{i}.md")),
291 format!("# Page {i}"),
292 )
293 .unwrap();
294 }
295
296 let budget = MemoryBudget {
297 max_bytes: 0,
298 batch_size: 10,
299 };
300 let batches = batched_content_files(&content, &budget).unwrap();
301
302 assert_eq!(batches.len(), 3); assert_eq!(batches[0].len(), 10);
304 assert_eq!(batches[1].len(), 10);
305 assert_eq!(batches[2].len(), 5);
306 }
307
308 #[test]
309 fn merge_dir_combines_files() {
310 let dir = tempdir().unwrap();
311 let src = dir.path().join("src");
312 let dst = dir.path().join("dst");
313 fs::create_dir_all(&src).unwrap();
314 fs::create_dir_all(&dst).unwrap();
315
316 fs::write(src.join("a.html"), "from src").unwrap();
317 fs::write(dst.join("b.html"), "existing").unwrap();
318
319 merge_dir(&src, &dst).unwrap();
320
321 assert_eq!(fs::read_to_string(dst.join("a.html")).unwrap(), "from src");
322 assert_eq!(fs::read_to_string(dst.join("b.html")).unwrap(), "existing");
323 }
324
325 #[test]
326 fn merge_dir_overwrites_on_conflict() {
327 let dir = tempdir().unwrap();
328 let src = dir.path().join("src");
329 let dst = dir.path().join("dst");
330 fs::create_dir_all(&src).unwrap();
331 fs::create_dir_all(&dst).unwrap();
332
333 fs::write(src.join("a.html"), "new").unwrap();
334 fs::write(dst.join("a.html"), "old").unwrap();
335
336 merge_dir(&src, &dst).unwrap();
337
338 assert_eq!(fs::read_to_string(dst.join("a.html")).unwrap(), "new");
339 }
340
341 #[test]
342 fn should_stream_when_explicitly_set() {
343 let dir = tempdir().unwrap();
344 let content = dir.path().join("content");
345 fs::create_dir_all(&content).unwrap();
346
347 let budget = MemoryBudget::default_budget();
348 assert!(should_stream(&content, &budget, true));
349 }
350
351 #[test]
352 fn compile_batch_empty_is_noop() {
353 let dir = tempdir().unwrap();
354 let result = compile_batch(
355 &[],
356 dir.path(),
357 &dir.path().join("build"),
358 &dir.path().join("site"),
359 &dir.path().join("templates"),
360 0,
361 );
362 assert!(result.is_ok());
363 }
364
365 #[test]
366 fn merge_dir_nonexistent_src_is_noop() {
367 let dir = tempdir().unwrap();
368 let result =
369 merge_dir(&dir.path().join("nonexistent"), &dir.path().join("dst"));
370 assert!(result.is_ok());
371 }
372
373 #[test]
374 fn merge_dir_nested() {
375 let dir = tempdir().unwrap();
376 let src = dir.path().join("src");
377 let dst = dir.path().join("dst");
378 let nested = src.join("sub");
379 fs::create_dir_all(&nested).unwrap();
380 fs::create_dir_all(&dst).unwrap();
381 fs::write(nested.join("file.txt"), "nested").unwrap();
382
383 merge_dir(&src, &dst).unwrap();
384 assert_eq!(
385 fs::read_to_string(dst.join("sub/file.txt")).unwrap(),
386 "nested"
387 );
388 }
389
390 #[test]
391 fn should_stream_large_site() {
392 let dir = tempdir().unwrap();
393 let content = dir.path().join("content");
394 fs::create_dir_all(&content).unwrap();
395 let budget = MemoryBudget {
398 max_bytes: 0,
399 batch_size: 2,
400 };
401 for i in 0..5 {
402 fs::write(content.join(format!("p{i}.md")), "# Hi").unwrap();
403 }
404 assert!(should_stream(&content, &budget, false));
405 }
406
407 #[test]
408 fn should_not_stream_small_site() {
409 let dir = tempdir().unwrap();
410 let content = dir.path().join("content");
411 fs::create_dir_all(&content).unwrap();
412 fs::write(content.join("index.md"), "# Home").unwrap();
413
414 let budget = MemoryBudget::default_budget();
415 assert!(!should_stream(&content, &budget, false));
416 }
417
418 #[test]
423 fn memory_budget_from_mb_one() {
424 let budget = MemoryBudget::from_mb(1);
425 assert_eq!(budget.max_bytes, 1024 * 1024);
426 assert_eq!(budget.batch_size, 16);
428 }
429
430 #[test]
431 fn memory_budget_from_mb_very_large() {
432 let budget = MemoryBudget::from_mb(4096);
433 assert_eq!(budget.max_bytes, 4096 * 1024 * 1024);
434 assert_eq!(budget.batch_size, 65536);
436 }
437
438 #[test]
439 fn memory_budget_batch_size_floor_is_ten() {
440 let budget = MemoryBudget::from_mb(0);
442 assert_eq!(budget.max_bytes, 0);
443 assert_eq!(budget.batch_size, 10);
444 }
445
446 #[test]
447 fn memory_budget_default_budget_matches_constant() {
448 let budget = MemoryBudget::default_budget();
449 assert_eq!(budget.max_bytes, DEFAULT_MEMORY_BUDGET_MB * 1024 * 1024);
450 assert_eq!(
451 budget.batch_size,
452 MemoryBudget::from_mb(DEFAULT_MEMORY_BUDGET_MB).batch_size
453 );
454 }
455
456 #[test]
457 fn memory_budget_clone_copy_debug() {
458 let a = MemoryBudget::from_mb(128);
459 let b = a; assert_eq!(a.max_bytes, b.max_bytes);
461 assert_eq!(a.batch_size, b.batch_size);
462 let debug = format!("{a:?}");
463 assert!(debug.contains("MemoryBudget"));
464 }
465
466 #[test]
471 fn batched_content_files_nonexistent_dir_returns_empty() {
472 let dir = tempdir().unwrap();
473 let budget = MemoryBudget::from_mb(512);
474 let result =
475 batched_content_files(&dir.path().join("nonexistent"), &budget);
476 let batches = result.unwrap_or_default();
479 assert!(batches.is_empty());
480 }
481
482 #[test]
483 fn batched_content_files_single_file() {
484 let dir = tempdir().unwrap();
485 let content = dir.path().join("content");
486 fs::create_dir_all(&content).unwrap();
487 fs::write(content.join("index.md"), "# Home").unwrap();
488
489 let budget = MemoryBudget::from_mb(512);
490 let batches = batched_content_files(&content, &budget).unwrap();
491 assert_eq!(batches.len(), 1);
492 assert_eq!(batches[0].len(), 1);
493 }
494
495 #[test]
496 fn batched_content_files_ignores_non_md() {
497 let dir = tempdir().unwrap();
498 let content = dir.path().join("content");
499 fs::create_dir_all(&content).unwrap();
500 fs::write(content.join("page.md"), "# Page").unwrap();
501 fs::write(content.join("image.png"), "fakepng").unwrap();
502 fs::write(content.join("style.css"), "body{}").unwrap();
503
504 let budget = MemoryBudget::from_mb(512);
505 let batches = batched_content_files(&content, &budget).unwrap();
506 let total: usize = batches.iter().map(|b| b.len()).sum();
507 assert_eq!(total, 1, "only .md files should be collected");
508 }
509
510 #[test]
511 fn batched_content_files_exact_batch_boundary() {
512 let dir = tempdir().unwrap();
513 let content = dir.path().join("content");
514 fs::create_dir_all(&content).unwrap();
515 for i in 0..10 {
516 fs::write(content.join(format!("p{i}.md")), "# Hi").unwrap();
517 }
518
519 let budget = MemoryBudget {
520 max_bytes: 0,
521 batch_size: 10,
522 };
523 let batches = batched_content_files(&content, &budget).unwrap();
524 assert_eq!(batches.len(), 1);
525 assert_eq!(batches[0].len(), 10);
526 }
527
528 #[test]
529 fn batched_content_files_many_small_batches() {
530 let dir = tempdir().unwrap();
531 let content = dir.path().join("content");
532 fs::create_dir_all(&content).unwrap();
533 for i in 0..7 {
534 fs::write(content.join(format!("p{i}.md")), "# Hi").unwrap();
535 }
536
537 let budget = MemoryBudget {
538 max_bytes: 0,
539 batch_size: 2,
540 };
541 let batches = batched_content_files(&content, &budget).unwrap();
542 assert_eq!(batches.len(), 4); assert_eq!(batches[3].len(), 1);
544 }
545
546 #[test]
547 fn batched_content_files_nested_directories() {
548 let dir = tempdir().unwrap();
549 let content = dir.path().join("content");
550 fs::create_dir_all(content.join("blog")).unwrap();
551 fs::create_dir_all(content.join("docs")).unwrap();
552 fs::write(content.join("index.md"), "# Index").unwrap();
553 fs::write(content.join("blog/post.md"), "# Post").unwrap();
554 fs::write(content.join("docs/api.md"), "# API").unwrap();
555
556 let budget = MemoryBudget::from_mb(512);
557 let batches = batched_content_files(&content, &budget).unwrap();
558 let total: usize = batches.iter().map(|b| b.len()).sum();
559 assert_eq!(total, 3);
560 }
561
562 #[test]
567 fn merge_dir_deeply_nested() {
568 let dir = tempdir().unwrap();
569 let src = dir.path().join("src");
570 let dst = dir.path().join("dst");
571 fs::create_dir_all(src.join("a/b/c")).unwrap();
572 fs::create_dir_all(&dst).unwrap();
573 fs::write(src.join("a/b/c/deep.txt"), "deep content").unwrap();
574
575 merge_dir(&src, &dst).unwrap();
576 assert_eq!(
577 fs::read_to_string(dst.join("a/b/c/deep.txt")).unwrap(),
578 "deep content"
579 );
580 }
581
582 #[test]
583 fn merge_dir_empty_src() {
584 let dir = tempdir().unwrap();
585 let src = dir.path().join("src");
586 let dst = dir.path().join("dst");
587 fs::create_dir_all(&src).unwrap();
588 fs::create_dir_all(&dst).unwrap();
589 fs::write(dst.join("existing.txt"), "keep").unwrap();
590
591 merge_dir(&src, &dst).unwrap();
592 assert_eq!(
593 fs::read_to_string(dst.join("existing.txt")).unwrap(),
594 "keep"
595 );
596 }
597
598 #[test]
599 fn merge_dir_multiple_files() {
600 let dir = tempdir().unwrap();
601 let src = dir.path().join("src");
602 let dst = dir.path().join("dst");
603 fs::create_dir_all(&src).unwrap();
604 fs::create_dir_all(&dst).unwrap();
605 for i in 0..5 {
606 fs::write(src.join(format!("f{i}.txt")), format!("data{i}"))
607 .unwrap();
608 }
609
610 merge_dir(&src, &dst).unwrap();
611 for i in 0..5 {
612 assert_eq!(
613 fs::read_to_string(dst.join(format!("f{i}.txt"))).unwrap(),
614 format!("data{i}")
615 );
616 }
617 }
618
619 #[test]
624 fn should_stream_with_no_content_dir() {
625 let dir = tempdir().unwrap();
626 let budget = MemoryBudget::from_mb(512);
627 assert!(!should_stream(
629 &dir.path().join("no-content"),
630 &budget,
631 false
632 ));
633 }
634
635 #[test]
636 fn should_stream_explicitly_set_overrides_count() {
637 let dir = tempdir().unwrap();
639 let content = dir.path().join("content");
640 fs::create_dir_all(&content).unwrap();
641
642 let budget = MemoryBudget::from_mb(512);
643 assert!(should_stream(&content, &budget, true));
644 }
645
646 #[test]
647 fn should_stream_exactly_at_batch_boundary() {
648 let dir = tempdir().unwrap();
649 let content = dir.path().join("content");
650 fs::create_dir_all(&content).unwrap();
651 let budget = MemoryBudget {
653 max_bytes: 0,
654 batch_size: 3,
655 };
656 for i in 0..3 {
657 fs::write(content.join(format!("p{i}.md")), "# Hi").unwrap();
658 }
659 assert!(!should_stream(&content, &budget, false));
661 }
662
663 #[test]
664 fn should_stream_one_over_boundary() {
665 let dir = tempdir().unwrap();
666 let content = dir.path().join("content");
667 fs::create_dir_all(&content).unwrap();
668 let budget = MemoryBudget {
669 max_bytes: 0,
670 batch_size: 3,
671 };
672 for i in 0..4 {
673 fs::write(content.join(format!("p{i}.md")), "# Hi").unwrap();
674 }
675 assert!(should_stream(&content, &budget, false));
677 }
678
679 #[test]
684 fn compile_batch_with_nonexistent_files_still_creates_dirs() {
685 let dir = tempdir().unwrap();
686 let content = dir.path().join("content");
687 let build = dir.path().join("build");
688 let site = dir.path().join("site");
689 let templates = dir.path().join("templates");
690 fs::create_dir_all(&content).unwrap();
691
692 let result = compile_batch(
695 &[content.join("nonexistent.md")],
696 &content,
697 &build,
698 &site,
699 &templates,
700 0,
701 );
702 let _ = result;
705 }
706
707 #[test]
708 fn compile_batch_creates_batch_content_dir() {
709 let dir = tempdir().unwrap();
710 let content = dir.path().join("content");
711 let build = dir.path().join("build");
712 let site = dir.path().join("site");
713 let templates = dir.path().join("templates");
714 fs::create_dir_all(&content).unwrap();
715 fs::create_dir_all(&templates).unwrap();
716 fs::write(content.join("page.md"), "---\ntitle: T\n---\n# Hi").unwrap();
717
718 let _result = compile_batch(
721 &[content.join("page.md")],
722 &content,
723 &build,
724 &site,
725 &templates,
726 42,
727 );
728 }
730
731 #[test]
732 fn compile_batch_succeeds_and_merges_valid_page() {
733 let dir = tempdir().unwrap();
737 let content = dir.path().join("content");
738 let build = dir.path().join("build");
739 let site = dir.path().join("public");
740 let templates = dir.path().join("templates");
741 fs::create_dir_all(&content).unwrap();
742 fs::create_dir_all(&build).unwrap();
743 fs::create_dir_all(&templates).unwrap();
744 fs::write(
745 content.join("index.md"),
746 "---\ntitle: \"Home\"\ndescription: \"home\"\n\
747 permalink: \"https://example.com/\"\n---\nhome body",
748 )
749 .unwrap();
750 fs::write(
751 templates.join("page.html"),
752 "<!doctype html><html><body>{{ content }}</body></html>",
753 )
754 .unwrap();
755
756 let result = compile_batch(
757 &[content.join("index.md")],
758 &content,
759 &build,
760 &site,
761 &templates,
762 7,
763 );
764 assert!(result.is_ok(), "expected successful batch: {result:?}");
765 assert!(site.is_dir(), "site dir must be created on success");
766 }
767
768 #[test]
769 fn compile_batch_fails_when_build_dir_is_a_file() {
770 let dir = tempdir().unwrap();
773 let content = dir.path().join("content");
774 let build_file = dir.path().join("build");
775 fs::create_dir_all(&content).unwrap();
776 fs::write(content.join("a.md"), "x").unwrap();
777 fs::write(&build_file, "i am a file").unwrap();
778
779 let result = compile_batch(
780 &[content.join("a.md")],
781 &content,
782 &build_file,
783 &dir.path().join("site"),
784 &dir.path().join("templates"),
785 0,
786 );
787 assert!(result.is_err());
788 }
789
790 #[test]
791 fn compile_batch_fails_when_dest_parent_blocked_by_file() {
792 let dir = tempdir().unwrap();
796 let content = dir.path().join("content");
797 let build = dir.path().join("build");
798 fs::create_dir_all(content.join("sub")).unwrap();
799 fs::write(content.join("sub/a.md"), "x").unwrap();
800 let batch_content = build.join(".batch-3");
801 fs::create_dir_all(&batch_content).unwrap();
802 fs::write(batch_content.join("sub"), "blocking file").unwrap();
803
804 let result = compile_batch(
805 &[content.join("sub/a.md")],
806 &content,
807 &build,
808 &dir.path().join("site"),
809 &dir.path().join("templates"),
810 3,
811 );
812 assert!(result.is_err());
813 }
814
815 #[test]
816 fn compile_batch_fails_when_batch_build_dir_blocked() {
817 let dir = tempdir().unwrap();
818 let content = dir.path().join("content");
819 let build = dir.path().join("build");
820 fs::create_dir_all(&content).unwrap();
821 fs::write(content.join("a.md"), "x").unwrap();
822 fs::create_dir_all(&build).unwrap();
823 fs::write(build.join(".batch-5-build"), "blocking file").unwrap();
825
826 let result = compile_batch(
827 &[content.join("a.md")],
828 &content,
829 &build,
830 &dir.path().join("site"),
831 &dir.path().join("templates"),
832 5,
833 );
834 assert!(result.is_err());
835 }
836
837 #[test]
838 fn compile_batch_fails_when_merge_dir_blocked_after_successful_compile() {
839 let dir = tempdir().unwrap();
849 let content = dir.path().join("content");
850 let build = dir.path().join("build");
851 let site = dir.path().join("public");
852 let templates = dir.path().join("templates");
853 fs::create_dir_all(&content).unwrap();
854 fs::create_dir_all(&build).unwrap();
855 fs::create_dir_all(&templates).unwrap();
856 fs::write(
857 content.join("index.md"),
858 "---\ntitle: \"Home\"\ndescription: \"home\"\n\
859 permalink: \"https://example.com/\"\n---\nhome body",
860 )
861 .unwrap();
862 fs::write(
863 content.join("about.md"),
864 "---\ntitle: \"About\"\ndescription: \"about\"\n\
865 permalink: \"https://example.com/about/\"\n---\nabout body",
866 )
867 .unwrap();
868 fs::write(
869 templates.join("page.html"),
870 "<!doctype html><html><body>{{ content }}</body></html>",
871 )
872 .unwrap();
873
874 fs::create_dir_all(&site).unwrap();
878 fs::write(site.join("about"), "blocking file, not a directory")
879 .unwrap();
880
881 let result = compile_batch(
882 &[content.join("index.md"), content.join("about.md")],
883 &content,
884 &build,
885 &site,
886 &templates,
887 11,
888 );
889 assert!(
890 result.is_err(),
891 "merge_dir failure after a successful compile must propagate: {result:?}"
892 );
893 }
894
895 #[test]
896 fn compile_batch_fails_when_batch_site_dir_blocked() {
897 let dir = tempdir().unwrap();
898 let content = dir.path().join("content");
899 let build = dir.path().join("build");
900 fs::create_dir_all(&content).unwrap();
901 fs::write(content.join("a.md"), "x").unwrap();
902 fs::create_dir_all(&build).unwrap();
903 fs::write(build.join(".batch-6-site"), "blocking file").unwrap();
904
905 let result = compile_batch(
906 &[content.join("a.md")],
907 &content,
908 &build,
909 &dir.path().join("site"),
910 &dir.path().join("templates"),
911 6,
912 );
913 assert!(result.is_err());
914 }
915
916 #[test]
921 fn merge_dir_src_is_file_fails_at_read_dir() {
922 let dir = tempdir().unwrap();
923 let src_file = dir.path().join("plain.txt");
924 fs::write(&src_file, "x").unwrap();
925
926 let result = merge_dir(&src_file, &dir.path().join("dst"));
927 assert!(result.is_err());
928 }
929
930 #[test]
931 fn merge_dir_subdir_blocked_by_file_in_dst() {
932 let dir = tempdir().unwrap();
933 let src = dir.path().join("src");
934 let dst = dir.path().join("dst");
935 fs::create_dir_all(src.join("sub")).unwrap();
936 fs::write(src.join("sub/f.html"), "x").unwrap();
937 fs::create_dir_all(&dst).unwrap();
938 fs::write(dst.join("sub"), "blocking file").unwrap();
939
940 let result = merge_dir(&src, &dst);
941 assert!(result.is_err());
942 }
943
944 #[test]
945 fn merge_dir_nested_failure_propagates_through_recursion() {
946 let dir = tempdir().unwrap();
949 let src = dir.path().join("src");
950 let dst = dir.path().join("dst");
951 fs::create_dir_all(src.join("a/b")).unwrap();
952 fs::write(src.join("a/b/f.html"), "x").unwrap();
953 fs::create_dir_all(dst.join("a")).unwrap();
954 fs::write(dst.join("a/b"), "blocking file").unwrap();
955
956 let result = merge_dir(&src, &dst);
957 assert!(result.is_err());
958 }
959
960 #[test]
961 fn merge_dir_copy_into_missing_dst_fails() {
962 let dir = tempdir().unwrap();
963 let src = dir.path().join("src");
964 fs::create_dir_all(&src).unwrap();
965 fs::write(src.join("f.html"), "x").unwrap();
966
967 let result = merge_dir(&src, &dir.path().join("missing-dst"));
969 assert!(result.is_err());
970 }
971}