@@ -6,6 +6,8 @@ use diecut_core::adapter::{self, TemplateFormat};
66use diecut_core:: config:: load_config;
77use diecut_core:: prompt:: PromptOptions ;
88use 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
1012fn 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"\x89 PNG\r \n \x1a \n \x00 \x00 \x00 \r IHDR" ;
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"\x89 PNG\r \n \x1a \n \x00 OLD" ;
568+ let new_binary = b"\x89 PNG\r \n \x1a \n \x00 NEW" ;
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