Skip to content

Commit e92fa74

Browse files
authored
Merge pull request #26 from raiderrobert/test/edge-case-coverage
test: add edge case integration tests for merge and source parsing
2 parents 2fb6e1a + 482f924 commit e92fa74

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/diecut-core/tests/integration.rs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use diecut_core::adapter::{self, TemplateFormat};
66
use diecut_core::config::load_config;
77
use diecut_core::prompt::PromptOptions;
88
use diecut_core::render::{build_context, build_context_with_namespace, walk_and_render};
9+
use diecut_core::template::source::{resolve_source, resolve_source_full};
10+
use diecut_core::update::merge::{three_way_merge, MergeAction};
911

1012
fn fixture_path(name: &str) -> PathBuf {
1113
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -533,3 +535,198 @@ fn test_migration_execute() {
533535
let migrated_resolved = adapter::resolve_template(output_dir.path()).unwrap();
534536
assert_eq!(migrated_resolved.format, TemplateFormat::Native);
535537
}
538+
539+
// --- Edge case: merge with binary files ---
540+
541+
#[test]
542+
fn test_three_way_merge_binary_files_unchanged() {
543+
let old_snap = tempfile::tempdir().unwrap();
544+
let new_snap = tempfile::tempdir().unwrap();
545+
let project = tempfile::tempdir().unwrap();
546+
547+
// Binary content (has null bytes)
548+
let binary = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR";
549+
std::fs::write(old_snap.path().join("logo.png"), binary).unwrap();
550+
std::fs::write(new_snap.path().join("logo.png"), binary).unwrap();
551+
std::fs::write(project.path().join("logo.png"), binary).unwrap();
552+
553+
let results = three_way_merge(project.path(), old_snap.path(), new_snap.path()).unwrap();
554+
// All identical → should produce no changes
555+
assert!(
556+
results.is_empty(),
557+
"identical binary files should produce no merge results"
558+
);
559+
}
560+
561+
#[test]
562+
fn test_three_way_merge_binary_file_updated_in_template() {
563+
let old_snap = tempfile::tempdir().unwrap();
564+
let new_snap = tempfile::tempdir().unwrap();
565+
let project = tempfile::tempdir().unwrap();
566+
567+
let old_binary = b"\x89PNG\r\n\x1a\n\x00OLD";
568+
let new_binary = b"\x89PNG\r\n\x1a\n\x00NEW";
569+
std::fs::write(old_snap.path().join("logo.png"), old_binary).unwrap();
570+
std::fs::write(new_snap.path().join("logo.png"), new_binary).unwrap();
571+
std::fs::write(project.path().join("logo.png"), old_binary).unwrap();
572+
573+
let results = three_way_merge(project.path(), old_snap.path(), new_snap.path()).unwrap();
574+
assert_eq!(results.len(), 1);
575+
assert_eq!(results[0].action, MergeAction::UpdateFromTemplate);
576+
}
577+
578+
// --- Edge case: merge with empty files ---
579+
580+
#[test]
581+
fn test_three_way_merge_empty_files() {
582+
let old_snap = tempfile::tempdir().unwrap();
583+
let new_snap = tempfile::tempdir().unwrap();
584+
let project = tempfile::tempdir().unwrap();
585+
586+
std::fs::write(old_snap.path().join("empty.txt"), "").unwrap();
587+
std::fs::write(new_snap.path().join("empty.txt"), "").unwrap();
588+
std::fs::write(project.path().join("empty.txt"), "").unwrap();
589+
590+
let results = three_way_merge(project.path(), old_snap.path(), new_snap.path()).unwrap();
591+
assert!(
592+
results.is_empty(),
593+
"identical empty files should be unchanged"
594+
);
595+
}
596+
597+
#[test]
598+
fn test_three_way_merge_empty_to_content() {
599+
let old_snap = tempfile::tempdir().unwrap();
600+
let new_snap = tempfile::tempdir().unwrap();
601+
let project = tempfile::tempdir().unwrap();
602+
603+
std::fs::write(old_snap.path().join("file.txt"), "").unwrap();
604+
std::fs::write(new_snap.path().join("file.txt"), "new content").unwrap();
605+
std::fs::write(project.path().join("file.txt"), "").unwrap();
606+
607+
let results = three_way_merge(project.path(), old_snap.path(), new_snap.path()).unwrap();
608+
assert_eq!(results.len(), 1);
609+
assert_eq!(results[0].action, MergeAction::UpdateFromTemplate);
610+
}
611+
612+
// --- Edge case: merge with nested directory changes ---
613+
614+
#[test]
615+
fn test_three_way_merge_nested_new_file() {
616+
let old_snap = tempfile::tempdir().unwrap();
617+
let new_snap = tempfile::tempdir().unwrap();
618+
let project = tempfile::tempdir().unwrap();
619+
620+
// Common file in all three
621+
std::fs::write(old_snap.path().join("root.txt"), "stable").unwrap();
622+
std::fs::write(new_snap.path().join("root.txt"), "stable").unwrap();
623+
std::fs::write(project.path().join("root.txt"), "stable").unwrap();
624+
625+
// New file in a nested directory only in new snapshot
626+
std::fs::create_dir_all(new_snap.path().join("sub/deep")).unwrap();
627+
std::fs::write(new_snap.path().join("sub/deep/new.txt"), "hello").unwrap();
628+
629+
let results = three_way_merge(project.path(), old_snap.path(), new_snap.path()).unwrap();
630+
let new_file = results
631+
.iter()
632+
.find(|r| r.rel_path.ends_with("sub/deep/new.txt"));
633+
assert!(new_file.is_some(), "should detect new nested file");
634+
assert_eq!(new_file.unwrap().action, MergeAction::AddFromTemplate);
635+
}
636+
637+
// --- Edge case: both sides converge to same content ---
638+
639+
#[test]
640+
fn test_three_way_merge_convergent_changes() {
641+
let old_snap = tempfile::tempdir().unwrap();
642+
let new_snap = tempfile::tempdir().unwrap();
643+
let project = tempfile::tempdir().unwrap();
644+
645+
std::fs::write(old_snap.path().join("file.txt"), "original").unwrap();
646+
// Both user and template independently changed to the same content
647+
std::fs::write(new_snap.path().join("file.txt"), "converged").unwrap();
648+
std::fs::write(project.path().join("file.txt"), "converged").unwrap();
649+
650+
let results = three_way_merge(project.path(), old_snap.path(), new_snap.path()).unwrap();
651+
// Should detect convergence → no conflict
652+
assert!(
653+
results.is_empty(),
654+
"convergent changes should produce no merge results"
655+
);
656+
}
657+
658+
// --- Edge case: template source URL parsing ---
659+
660+
#[test]
661+
fn test_resolve_source_rejects_empty_abbreviation_remainder() {
662+
assert!(resolve_source("gh:").is_err());
663+
assert!(resolve_source("gl:").is_err());
664+
assert!(resolve_source("bb:").is_err());
665+
assert!(resolve_source("sr:").is_err());
666+
}
667+
668+
#[test]
669+
fn test_resolve_source_user_abbreviation_empty_remainder() {
670+
let mut abbrevs = std::collections::HashMap::new();
671+
abbrevs.insert("co".to_string(), "https://git.co.com/{}.git".to_string());
672+
assert!(resolve_source_full("co:", None, Some(&abbrevs)).is_err());
673+
}
674+
675+
// --- Edge case: unsupported template format ---
676+
677+
#[test]
678+
fn test_resolve_template_unsupported_format() {
679+
let tmp = tempfile::tempdir().unwrap();
680+
// No diecut.toml or cookiecutter.json
681+
std::fs::write(tmp.path().join("random.txt"), "not a template").unwrap();
682+
683+
let result = adapter::resolve_template(tmp.path());
684+
assert!(result.is_err(), "should fail for unsupported format");
685+
}
686+
687+
// --- Edge case: render with special characters in variable values ---
688+
689+
#[test]
690+
fn test_render_with_special_characters() {
691+
let template_dir = fixture_path("basic-template");
692+
let resolved = adapter::resolve_template(&template_dir).unwrap();
693+
694+
let mut variables = default_variables();
695+
// Use a name with special characters that could trip up template engines
696+
variables.insert(
697+
"project_name".to_string(),
698+
tera::Value::String("my-project_v2.0".to_string()),
699+
);
700+
variables.insert(
701+
"project_slug".to_string(),
702+
tera::Value::String("my-project_v2.0".to_string()),
703+
);
704+
705+
let context = build_context(&variables);
706+
let output_dir = tempfile::tempdir().unwrap();
707+
let result = walk_and_render(&resolved, output_dir.path(), &variables, &context);
708+
assert!(result.is_ok(), "should handle special characters in values");
709+
710+
// Verify the rendered output actually contains the special character value
711+
let project_dir = output_dir.path().join("my-project_v2.0");
712+
assert!(
713+
project_dir.exists(),
714+
"project directory with special characters should exist"
715+
);
716+
717+
let readme = project_dir.join("README.md");
718+
assert!(readme.exists(), "README.md should exist");
719+
let readme_content = std::fs::read_to_string(&readme).unwrap();
720+
assert!(
721+
readme_content.contains("my-project_v2.0"),
722+
"README should contain the special character project name, got: {readme_content}"
723+
);
724+
725+
let cargo_toml = project_dir.join("Cargo.toml");
726+
assert!(cargo_toml.exists(), "Cargo.toml should exist");
727+
let cargo_content = std::fs::read_to_string(&cargo_toml).unwrap();
728+
assert!(
729+
cargo_content.contains("my-project_v2.0"),
730+
"Cargo.toml should contain the special character project name, got: {cargo_content}"
731+
);
732+
}

0 commit comments

Comments
 (0)