465 lines
14 KiB
Go
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))
|
|
}
|
|
}
|