package sync import ( "os" "path/filepath" "strings" "testing" "sub-cli/internal/model" ) func TestSyncASSTimeline(t *testing.T) { testCases := []struct { name string source model.ASSFile target model.ASSFile verify func(t *testing.T, result model.ASSFile) }{ { name: "Equal event counts", source: model.ASSFile{ ScriptInfo: map[string]string{"Title": "Source ASS"}, Styles: []model.ASSStyle{ { Name: "Default", Properties: map[string]string{ "Format": "Name, Fontname, Fontsize, PrimaryColour", "Style": "Default,Arial,20,&H00FFFFFF", }, }, }, Events: []model.ASSEvent{ { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 1}, EndTime: model.Timestamp{Seconds: 4}, Style: "Default", Text: "Source line one.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 5}, EndTime: model.Timestamp{Seconds: 8}, Style: "Default", Text: "Source line two.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 9}, EndTime: model.Timestamp{Seconds: 12}, Style: "Default", Text: "Source line three.", }, }, }, target: model.ASSFile{ ScriptInfo: map[string]string{"Title": "Target ASS"}, Styles: []model.ASSStyle{ { Name: "Default", Properties: map[string]string{ "Format": "Name, Fontname, Fontsize, PrimaryColour", "Style": "Default,Arial,20,&H00FFFFFF", }, }, { Name: "Alternate", Properties: map[string]string{ "Format": "Name, Fontname, Fontsize, PrimaryColour", "Style": "Alternate,Times New Roman,20,&H0000FFFF", }, }, }, Events: []model.ASSEvent{ { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Minutes: 1, Seconds: 0}, EndTime: model.Timestamp{Minutes: 1, Seconds: 3}, Style: "Default", Text: "Target line one.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Minutes: 1, Seconds: 5}, EndTime: model.Timestamp{Minutes: 1, Seconds: 8}, Style: "Alternate", Text: "Target line two.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Minutes: 1, Seconds: 10}, EndTime: model.Timestamp{Minutes: 1, Seconds: 13}, Style: "Default", Text: "Target line three.", }, }, }, verify: func(t *testing.T, result model.ASSFile) { if len(result.Events) != 3 { t.Errorf("Expected 3 events, got %d", len(result.Events)) return } // Check that source timings are applied to target events if result.Events[0].StartTime.Seconds != 1 || result.Events[0].EndTime.Seconds != 4 { t.Errorf("First event timing mismatch: got %+v", result.Events[0]) } if result.Events[1].StartTime.Seconds != 5 || result.Events[1].EndTime.Seconds != 8 { t.Errorf("Second event timing mismatch: got %+v", result.Events[1]) } if result.Events[2].StartTime.Seconds != 9 || result.Events[2].EndTime.Seconds != 12 { t.Errorf("Third event timing mismatch: got %+v", result.Events[2]) } // Check that target content and styles are preserved if result.Events[0].Text != "Target line one." { t.Errorf("Content should be preserved, got: %s", result.Events[0].Text) } if result.Events[1].Style != "Alternate" { t.Errorf("Style should be preserved, got: %s", result.Events[1].Style) } // Check that script info and style definitions are preserved if result.ScriptInfo["Title"] != "Target ASS" { t.Errorf("ScriptInfo should be preserved, got: %+v", result.ScriptInfo) } if len(result.Styles) != 2 { t.Errorf("Expected 2 styles, got %d", len(result.Styles)) } if result.Styles[1].Name != "Alternate" { t.Errorf("Style definitions should be preserved, got: %+v", result.Styles[1]) } }, }, { name: "More target events than source", source: model.ASSFile{ Events: []model.ASSEvent{ { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 1}, EndTime: model.Timestamp{Seconds: 4}, Style: "Default", Text: "Source line one.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 5}, EndTime: model.Timestamp{Seconds: 8}, Style: "Default", Text: "Source line two.", }, }, }, target: model.ASSFile{ Events: []model.ASSEvent{ { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Minutes: 1, Seconds: 0}, EndTime: model.Timestamp{Minutes: 1, Seconds: 3}, Style: "Default", Text: "Target line one.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Minutes: 1, Seconds: 5}, EndTime: model.Timestamp{Minutes: 1, Seconds: 8}, Style: "Default", Text: "Target line two.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Minutes: 1, Seconds: 10}, EndTime: model.Timestamp{Minutes: 1, Seconds: 13}, Style: "Default", Text: "Target line three.", }, }, }, verify: func(t *testing.T, result model.ASSFile) { if len(result.Events) != 3 { t.Errorf("Expected 3 events, got %d", len(result.Events)) return } // First event should use first source timing if result.Events[0].StartTime.Seconds != 1 { t.Errorf("First event should have first source timing, got: %+v", result.Events[0].StartTime) } // Last event should use last source timing if result.Events[2].StartTime.Seconds != 5 { t.Errorf("Last event should have last source timing, got: %+v", result.Events[2].StartTime) } // Verify content is preserved if result.Events[2].Text != "Target line three." { t.Errorf("Content should be preserved, got: %s", result.Events[2].Text) } }, }, { name: "More source events than target", source: model.ASSFile{ Events: []model.ASSEvent{ { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 1}, EndTime: model.Timestamp{Seconds: 3}, Style: "Default", Text: "Source line one.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 4}, EndTime: model.Timestamp{Seconds: 6}, Style: "Default", Text: "Source line two.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 7}, EndTime: model.Timestamp{Seconds: 9}, Style: "Default", Text: "Source line three.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 10}, EndTime: model.Timestamp{Seconds: 12}, Style: "Default", Text: "Source line four.", }, }, }, target: model.ASSFile{ Events: []model.ASSEvent{ { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Minutes: 1, Seconds: 0}, EndTime: model.Timestamp{Minutes: 1, Seconds: 3}, Style: "Default", Text: "Target line one.", }, { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Minutes: 1, Seconds: 5}, EndTime: model.Timestamp{Minutes: 1, Seconds: 8}, Style: "Default", Text: "Target line two.", }, }, }, verify: func(t *testing.T, result model.ASSFile) { if len(result.Events) != 2 { t.Errorf("Expected 2 events, got %d", len(result.Events)) return } // First event should have first source timing if result.Events[0].StartTime.Seconds != 1 { t.Errorf("First event should have first source timing, got: %+v", result.Events[0].StartTime) } // Last event should have last source timing if result.Events[1].StartTime.Seconds != 10 { t.Errorf("Last event should have last source timing, got: %+v", result.Events[1].StartTime) } // Check that target content is preserved if result.Events[0].Text != "Target line one." || result.Events[1].Text != "Target line two." { t.Errorf("Content should be preserved, got: %+v", result.Events) } }, }, { name: "Empty target events", source: model.ASSFile{ Events: []model.ASSEvent{ { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 1}, EndTime: model.Timestamp{Seconds: 4}, Style: "Default", Text: "Source line one.", }, }, }, target: model.ASSFile{ ScriptInfo: map[string]string{"Title": "Empty Target"}, Events: []model.ASSEvent{}, }, verify: func(t *testing.T, result model.ASSFile) { if len(result.Events) != 0 { t.Errorf("Expected 0 events, got %d", len(result.Events)) } // ScriptInfo should be preserved if result.ScriptInfo["Title"] != "Empty Target" { t.Errorf("ScriptInfo should be preserved, got: %+v", result.ScriptInfo) } }, }, { name: "Empty source events", source: model.ASSFile{ Events: []model.ASSEvent{}, }, target: model.ASSFile{ ScriptInfo: map[string]string{"Title": "Target with content"}, Events: []model.ASSEvent{ { Type: "Dialogue", Layer: 0, StartTime: model.Timestamp{Seconds: 10}, EndTime: model.Timestamp{Seconds: 15}, Style: "Default", Text: "Target line one.", }, }, }, verify: func(t *testing.T, result model.ASSFile) { if len(result.Events) != 1 { t.Errorf("Expected 1 event, got %d", len(result.Events)) return } // Timing should be preserved since source is empty if result.Events[0].StartTime.Seconds != 10 || result.Events[0].EndTime.Seconds != 15 { t.Errorf("Timing should match target when source is empty, got: %+v", result.Events[0]) } // Content should be preserved if result.Events[0].Text != "Target line one." { t.Errorf("Content should be preserved, got: %s", result.Events[0].Text) } // Title should be preserved if result.ScriptInfo["Title"] != "Target with content" { t.Errorf("Title should be preserved, got: %+v", result.ScriptInfo) } }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := syncASSTimeline(tc.source, tc.target) if tc.verify != nil { tc.verify(t, result) } }) } } func TestSyncASSFiles(t *testing.T) { // Create temporary test directory tempDir := t.TempDir() // Test case for testing the sync of ASS files sourceContent := `[Script Info] ScriptType: v4.00+ PlayResX: 640 PlayResY: 480 Timer: 100.0000 Title: Source ASS File [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Dialogue: 0,0:00:01.00,0:00:04.00,Default,,0,0,0,,Source line one. Dialogue: 0,0:00:05.00,0:00:08.00,Default,,0,0,0,,Source line two. Dialogue: 0,0:00:09.00,0:00:12.00,Default,,0,0,0,,Source line three. ` targetContent := `[Script Info] ScriptType: v4.00+ PlayResX: 640 PlayResY: 480 Timer: 100.0000 Title: Target ASS File [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1 Style: Alternate,Arial,20,&H0000FFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Dialogue: 0,0:01:00.00,0:01:03.00,Default,,0,0,0,,Target line one. Dialogue: 0,0:01:05.00,0:01:08.00,Alternate,,0,0,0,,Target line two. Dialogue: 0,0:01:10.00,0:01:13.00,Default,,0,0,0,,Target line three. ` sourceFile := filepath.Join(tempDir, "source.ass") targetFile := filepath.Join(tempDir, "target.ass") // Write test files if err := os.WriteFile(sourceFile, []byte(sourceContent), 0644); err != nil { t.Fatalf("Failed to write source file: %v", err) } if err := os.WriteFile(targetFile, []byte(targetContent), 0644); err != nil { t.Fatalf("Failed to write target file: %v", err) } // Run syncASSFiles err := syncASSFiles(sourceFile, targetFile) if err != nil { t.Fatalf("syncASSFiles returned error: %v", err) } // Read the modified target file modifiedContent, err := os.ReadFile(targetFile) if err != nil { t.Fatalf("Failed to read modified file: %v", err) } // Verify the result // Should have source timings if !strings.Contains(string(modifiedContent), "0:00:01.00") { t.Errorf("Output should have source timing 0:00:01.00, got: %s", string(modifiedContent)) } // Should preserve target content and styles if !strings.Contains(string(modifiedContent), "Target line one.") { t.Errorf("Output should preserve target content, got: %s", string(modifiedContent)) } if !strings.Contains(string(modifiedContent), "Style: Alternate") { t.Errorf("Output should preserve target styles, got: %s", string(modifiedContent)) } // Should preserve title if !strings.Contains(string(modifiedContent), "Title: Target ASS File") { t.Errorf("Output should preserve target title, got: %s", string(modifiedContent)) } }