sub-cli/internal/sync/ass_test.go
2025-04-23 19:22:41 +08:00

465 lines
14 KiB
Go

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))
}
}