1241 lines
37 KiB
Go
1241 lines
37 KiB
Go
package sync
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sub-cli/internal/model"
|
|
)
|
|
|
|
func TestSyncLyrics(t *testing.T) {
|
|
// Create temporary test directory
|
|
tempDir := t.TempDir()
|
|
|
|
// Test cases for different format combinations
|
|
testCases := []struct {
|
|
name string
|
|
sourceContent string
|
|
sourceExt string
|
|
targetContent string
|
|
targetExt string
|
|
expectedError bool
|
|
validateOutput func(t *testing.T, filePath string)
|
|
}{
|
|
{
|
|
name: "LRC to LRC sync",
|
|
sourceContent: `[ti:Source LRC]
|
|
[ar:Test Artist]
|
|
|
|
[00:01.00]This is line one.
|
|
[00:05.00]This is line two.
|
|
[00:09.50]This is line three.
|
|
`,
|
|
sourceExt: "lrc",
|
|
targetContent: `[ti:Target LRC]
|
|
[ar:Different Artist]
|
|
|
|
[00:10.00]This is line one with different timing.
|
|
[00:20.00]This is line two with different timing.
|
|
[00:30.00]This is line three with different timing.
|
|
`,
|
|
targetExt: "lrc",
|
|
expectedError: false,
|
|
validateOutput: func(t *testing.T, filePath string) {
|
|
content, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read output file: %v", err)
|
|
}
|
|
|
|
contentStr := string(content)
|
|
|
|
// Should contain target title but source timings
|
|
if !strings.Contains(contentStr, "[ti:Target LRC]") {
|
|
t.Errorf("Output should preserve target title, got: %s", contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, "[ar:Different Artist]") {
|
|
t.Errorf("Output should preserve target artist, got: %s", contentStr)
|
|
}
|
|
|
|
// Should have source timings
|
|
if !strings.Contains(contentStr, "[00:01.000]") {
|
|
t.Errorf("Output should have source timing [00:01.000], got: %s", contentStr)
|
|
}
|
|
|
|
// Should have target content
|
|
if !strings.Contains(contentStr, "This is line one with different timing.") {
|
|
t.Errorf("Output should preserve target content, got: %s", contentStr)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "SRT to SRT sync",
|
|
sourceContent: `1
|
|
00:00:01,000 --> 00:00:04,000
|
|
This is line one.
|
|
|
|
2
|
|
00:00:05,000 --> 00:00:08,000
|
|
This is line two.
|
|
|
|
3
|
|
00:00:09,000 --> 00:00:12,000
|
|
This is line three.
|
|
`,
|
|
sourceExt: "srt",
|
|
targetContent: `1
|
|
00:01:00,000 --> 00:01:03,000
|
|
This is target line one.
|
|
|
|
2
|
|
00:01:05,000 --> 00:01:08,000
|
|
This is target line two.
|
|
|
|
3
|
|
00:01:10,000 --> 00:01:13,000
|
|
This is target line three.
|
|
`,
|
|
targetExt: "srt",
|
|
expectedError: false,
|
|
validateOutput: func(t *testing.T, filePath string) {
|
|
content, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read output file: %v", err)
|
|
}
|
|
|
|
contentStr := string(content)
|
|
|
|
// Should have source timings but target content
|
|
if !strings.Contains(contentStr, "00:00:01,000 -->") {
|
|
t.Errorf("Output should have source timing 00:00:01,000, got: %s", contentStr)
|
|
}
|
|
|
|
// Check target content is preserved
|
|
if !strings.Contains(contentStr, "This is target line one.") {
|
|
t.Errorf("Output should preserve target content, got: %s", contentStr)
|
|
}
|
|
|
|
// Check identifiers are sequential
|
|
if !strings.Contains(contentStr, "1\n00:00:01,000") {
|
|
t.Errorf("Output should have sequential identifiers starting with 1, got: %s", contentStr)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "VTT to VTT sync",
|
|
sourceContent: `WEBVTT
|
|
|
|
1
|
|
00:00:01.000 --> 00:00:04.000
|
|
This is line one.
|
|
|
|
2
|
|
00:00:05.000 --> 00:00:08.000
|
|
This is line two.
|
|
|
|
3
|
|
00:00:09.000 --> 00:00:12.000
|
|
This is line three.
|
|
`,
|
|
sourceExt: "vtt",
|
|
targetContent: `WEBVTT - Target Title
|
|
|
|
1
|
|
00:01:00.000 --> 00:01:03.000 align:start position:10%
|
|
This is target line one.
|
|
|
|
2
|
|
00:01:05.000 --> 00:01:08.000 align:middle
|
|
This is target line two.
|
|
|
|
3
|
|
00:01:10.000 --> 00:01:13.000
|
|
This is target line three.
|
|
`,
|
|
targetExt: "vtt",
|
|
expectedError: false,
|
|
validateOutput: func(t *testing.T, filePath string) {
|
|
content, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read output file: %v", err)
|
|
}
|
|
|
|
contentStr := string(content)
|
|
|
|
// Should preserve VTT title
|
|
if !strings.Contains(contentStr, "WEBVTT - Target Title") {
|
|
t.Errorf("Output should preserve target title, got: %s", contentStr)
|
|
}
|
|
|
|
// Should have source timings
|
|
if !strings.Contains(contentStr, "00:00:01.000 -->") {
|
|
t.Errorf("Output should have source timing 00:00:01.000, got: %s", contentStr)
|
|
}
|
|
|
|
// Should preserve styling - don't check exact order, just presence of attributes
|
|
if !strings.Contains(contentStr, "align:start") || !strings.Contains(contentStr, "position:10%") {
|
|
t.Errorf("Output should preserve both cue settings (align:start and position:10%%), got: %s", contentStr)
|
|
}
|
|
|
|
// Should preserve target content
|
|
if !strings.Contains(contentStr, "This is target line one.") {
|
|
t.Errorf("Output should preserve target content, got: %s", contentStr)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "ASS to ASS sync",
|
|
sourceContent: `[Script Info]
|
|
ScriptType: v4.00+
|
|
PlayResX: 640
|
|
PlayResY: 480
|
|
Title: Source ASS
|
|
|
|
[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.
|
|
`,
|
|
sourceExt: "ass",
|
|
targetContent: `[Script Info]
|
|
ScriptType: v4.00+
|
|
PlayResX: 640
|
|
PlayResY: 480
|
|
Title: Target ASS
|
|
|
|
[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:01:00.00,0:01:03.00,Default,,0,0,0,,Target line one.
|
|
Dialogue: 0,0:01:05.00,0:01:08.00,Default,,0,0,0,,Target line two.
|
|
Dialogue: 0,0:01:10.00,0:01:13.00,Default,,0,0,0,,Target line three.
|
|
`,
|
|
targetExt: "ass",
|
|
expectedError: false,
|
|
validateOutput: func(t *testing.T, filePath string) {
|
|
content, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read output file: %v", err)
|
|
}
|
|
|
|
contentStr := string(content)
|
|
|
|
// Should preserve script info from target
|
|
if !strings.Contains(contentStr, "Title: Target ASS") {
|
|
t.Errorf("Output should preserve target title, got: %s", contentStr)
|
|
}
|
|
|
|
// Should have source timings but target content
|
|
if !strings.Contains(contentStr, "0:00:01.00,0:00:04.00") {
|
|
t.Errorf("Output should have source timing 0:00:01.00, got: %s", contentStr)
|
|
}
|
|
|
|
// Check target content is preserved
|
|
if !strings.Contains(contentStr, "Target line one.") {
|
|
t.Errorf("Output should preserve target content, got: %s", contentStr)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
// Run test cases
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create source file
|
|
sourceFile := filepath.Join(tempDir, "source."+tc.sourceExt)
|
|
if err := os.WriteFile(sourceFile, []byte(tc.sourceContent), 0644); err != nil {
|
|
t.Fatalf("Failed to create source file: %v", err)
|
|
}
|
|
|
|
// Create target file
|
|
targetFile := filepath.Join(tempDir, "target."+tc.targetExt)
|
|
if err := os.WriteFile(targetFile, []byte(tc.targetContent), 0644); err != nil {
|
|
t.Fatalf("Failed to create target file: %v", err)
|
|
}
|
|
|
|
// Call SyncLyrics
|
|
err := SyncLyrics(sourceFile, targetFile)
|
|
|
|
// Check error
|
|
if tc.expectedError && err == nil {
|
|
t.Errorf("Expected error but got none")
|
|
}
|
|
if !tc.expectedError && err != nil {
|
|
t.Errorf("Expected no error but got: %v", err)
|
|
}
|
|
|
|
// If no error expected and validation function provided, validate output
|
|
if !tc.expectedError && tc.validateOutput != nil {
|
|
// Make sure file exists
|
|
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
|
|
t.Fatalf("Target file was not created: %v", err)
|
|
}
|
|
|
|
tc.validateOutput(t, targetFile)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test unsupported format
|
|
t.Run("Unsupported format", func(t *testing.T) {
|
|
sourceFile := filepath.Join(tempDir, "source.unknown")
|
|
targetFile := filepath.Join(tempDir, "target.unknown")
|
|
|
|
// Create source and target files
|
|
sourceContent := "Some content in unknown format"
|
|
if err := os.WriteFile(sourceFile, []byte(sourceContent), 0644); err != nil {
|
|
t.Fatalf("Failed to create source file: %v", err)
|
|
}
|
|
|
|
targetContent := "Some target content in unknown format"
|
|
if err := os.WriteFile(targetFile, []byte(targetContent), 0644); err != nil {
|
|
t.Fatalf("Failed to create target file: %v", err)
|
|
}
|
|
|
|
// Call SyncLyrics, expect error
|
|
if err := SyncLyrics(sourceFile, targetFile); err == nil {
|
|
t.Errorf("Expected error for unsupported format, but got none")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSyncASSTimeline(t *testing.T) {
|
|
t.Run("Equal number of events", func(t *testing.T) {
|
|
// Create source ASS file
|
|
source := model.ASSFile{
|
|
ScriptInfo: map[string]string{
|
|
"Title": "Source ASS",
|
|
"ScriptType": "v4.00+",
|
|
},
|
|
Styles: []model.ASSStyle{
|
|
{
|
|
Name: "Default",
|
|
Properties: map[string]string{
|
|
"Bold": "0",
|
|
},
|
|
},
|
|
},
|
|
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.",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create target ASS file
|
|
target := model.ASSFile{
|
|
ScriptInfo: map[string]string{
|
|
"Title": "Target ASS",
|
|
"ScriptType": "v4.00+",
|
|
},
|
|
Styles: []model.ASSStyle{
|
|
{
|
|
Name: "Default",
|
|
Properties: map[string]string{
|
|
"Bold": "0",
|
|
},
|
|
},
|
|
},
|
|
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.",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Sync the timelines
|
|
result := syncASSTimeline(source, target)
|
|
|
|
// Check that the result has the correct number of events
|
|
if len(result.Events) != 2 {
|
|
t.Errorf("Expected 2 events, got %d", len(result.Events))
|
|
}
|
|
|
|
// Check that the script info was preserved from the target
|
|
if result.ScriptInfo["Title"] != "Target ASS" {
|
|
t.Errorf("Expected title 'Target ASS', got '%s'", result.ScriptInfo["Title"])
|
|
}
|
|
|
|
// Check that the first event has the source timing but target text
|
|
if result.Events[0].StartTime.Seconds != 1 {
|
|
t.Errorf("Expected start time 1 second, got %d", result.Events[0].StartTime.Seconds)
|
|
}
|
|
if result.Events[0].Text != "Target line one." {
|
|
t.Errorf("Expected text 'Target line one.', got '%s'", result.Events[0].Text)
|
|
}
|
|
|
|
// Check that the second event has the source timing but target text
|
|
if result.Events[1].StartTime.Seconds != 5 {
|
|
t.Errorf("Expected start time 5 seconds, got %d", result.Events[1].StartTime.Seconds)
|
|
}
|
|
if result.Events[1].Text != "Target line two." {
|
|
t.Errorf("Expected text 'Target line two.', got '%s'", result.Events[1].Text)
|
|
}
|
|
})
|
|
|
|
t.Run("Different number of events", func(t *testing.T) {
|
|
// Create source ASS file with 3 events
|
|
source := model.ASSFile{
|
|
ScriptInfo: map[string]string{
|
|
"Title": "Source ASS",
|
|
"ScriptType": "v4.00+",
|
|
},
|
|
Events: []model.ASSEvent{
|
|
{
|
|
Type: "Dialogue",
|
|
Layer: 0,
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Text: "Source line one.",
|
|
},
|
|
{
|
|
Type: "Dialogue",
|
|
Layer: 0,
|
|
StartTime: model.Timestamp{Seconds: 5},
|
|
EndTime: model.Timestamp{Seconds: 8},
|
|
Text: "Source line two.",
|
|
},
|
|
{
|
|
Type: "Dialogue",
|
|
Layer: 0,
|
|
StartTime: model.Timestamp{Seconds: 9},
|
|
EndTime: model.Timestamp{Seconds: 12},
|
|
Text: "Source line three.",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create target ASS file with 2 events
|
|
target := model.ASSFile{
|
|
ScriptInfo: map[string]string{
|
|
"Title": "Target ASS",
|
|
"ScriptType": "v4.00+",
|
|
},
|
|
Events: []model.ASSEvent{
|
|
{
|
|
Type: "Dialogue",
|
|
Layer: 0,
|
|
StartTime: model.Timestamp{Minutes: 1, Seconds: 0},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 3},
|
|
Text: "Target line one.",
|
|
},
|
|
{
|
|
Type: "Dialogue",
|
|
Layer: 0,
|
|
StartTime: model.Timestamp{Minutes: 1, Seconds: 5},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 8},
|
|
Text: "Target line two.",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Sync the timelines
|
|
result := syncASSTimeline(source, target)
|
|
|
|
// Check that the result has the correct number of events
|
|
if len(result.Events) != 2 {
|
|
t.Errorf("Expected 2 events, got %d", len(result.Events))
|
|
}
|
|
|
|
// Timeline should be scaled
|
|
if result.Events[0].StartTime.Seconds != 1 {
|
|
t.Errorf("Expected first event start time 1 second, got %d", result.Events[0].StartTime.Seconds)
|
|
}
|
|
|
|
// With 3 source events and 2 target events, the second event should get timing from the third source event
|
|
if result.Events[1].StartTime.Seconds != 9 {
|
|
t.Errorf("Expected second event start time 9 seconds, got %d", result.Events[1].StartTime.Seconds)
|
|
}
|
|
})
|
|
|
|
t.Run("Empty events", func(t *testing.T) {
|
|
// Create source and target with empty events
|
|
source := model.ASSFile{
|
|
ScriptInfo: map[string]string{"Title": "Source ASS"},
|
|
Events: []model.ASSEvent{},
|
|
}
|
|
|
|
target := model.ASSFile{
|
|
ScriptInfo: map[string]string{"Title": "Target ASS"},
|
|
Events: []model.ASSEvent{},
|
|
}
|
|
|
|
// Sync the timelines
|
|
result := syncASSTimeline(source, target)
|
|
|
|
// Check that the result has empty events
|
|
if len(result.Events) != 0 {
|
|
t.Errorf("Expected 0 events, got %d", len(result.Events))
|
|
}
|
|
|
|
// Check that the script info was preserved
|
|
if result.ScriptInfo["Title"] != "Target ASS" {
|
|
t.Errorf("Expected title 'Target ASS', got '%s'", result.ScriptInfo["Title"])
|
|
}
|
|
})
|
|
|
|
t.Run("Source has events, target is empty", func(t *testing.T) {
|
|
// Create source with events
|
|
source := model.ASSFile{
|
|
ScriptInfo: map[string]string{"Title": "Source ASS"},
|
|
Events: []model.ASSEvent{
|
|
{
|
|
Type: "Dialogue",
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Text: "Source line.",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create target with no events
|
|
target := model.ASSFile{
|
|
ScriptInfo: map[string]string{"Title": "Target ASS"},
|
|
Events: []model.ASSEvent{},
|
|
}
|
|
|
|
// Sync the timelines
|
|
result := syncASSTimeline(source, target)
|
|
|
|
// Result should have no events
|
|
if len(result.Events) != 0 {
|
|
t.Errorf("Expected 0 events, got %d", len(result.Events))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSyncASSFiles(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
t.Run("Sync ASS files", func(t *testing.T) {
|
|
// Create source ASS file
|
|
sourceFile := filepath.Join(tempDir, "source.ass")
|
|
sourceContent := `[Script Info]
|
|
ScriptType: v4.00+
|
|
PlayResX: 640
|
|
PlayResY: 480
|
|
Title: Source ASS
|
|
|
|
[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.
|
|
`
|
|
if err := os.WriteFile(sourceFile, []byte(sourceContent), 0644); err != nil {
|
|
t.Fatalf("Failed to create source file: %v", err)
|
|
}
|
|
|
|
// Create target ASS file
|
|
targetFile := filepath.Join(tempDir, "target.ass")
|
|
targetContent := `[Script Info]
|
|
ScriptType: v4.00+
|
|
PlayResX: 640
|
|
PlayResY: 480
|
|
Title: Target ASS
|
|
|
|
[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:01:00.00,0:01:03.00,Default,,0,0,0,,Target line one.
|
|
Dialogue: 0,0:01:05.00,0:01:08.00,Default,,0,0,0,,Target line two.
|
|
Dialogue: 0,0:01:10.00,0:01:13.00,Default,,0,0,0,,Target line three.
|
|
`
|
|
if err := os.WriteFile(targetFile, []byte(targetContent), 0644); err != nil {
|
|
t.Fatalf("Failed to create target file: %v", err)
|
|
}
|
|
|
|
// Sync the files
|
|
err := syncASSFiles(sourceFile, targetFile)
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got: %v", err)
|
|
}
|
|
|
|
// Check that the target file exists
|
|
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
|
|
t.Errorf("Target file no longer exists: %v", err)
|
|
}
|
|
|
|
// Check the contents of the target file
|
|
outputContent, err := os.ReadFile(targetFile)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read target file: %v", err)
|
|
}
|
|
|
|
outputContentStr := string(outputContent)
|
|
|
|
// Should preserve script info from target
|
|
if !strings.Contains(outputContentStr, "Title: Target ASS") {
|
|
t.Errorf("Output should preserve target title, got: %s", outputContentStr)
|
|
}
|
|
|
|
// Should have source timings but target content
|
|
if !strings.Contains(outputContentStr, "0:00:01.00,0:00:04.00") {
|
|
t.Errorf("Output should have source timing 0:00:01.00, got: %s", outputContentStr)
|
|
}
|
|
|
|
// Should have target content
|
|
if !strings.Contains(outputContentStr, "Target line one.") {
|
|
t.Errorf("Output should preserve target content, got: %s", outputContentStr)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSyncVTTTimeline(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
source model.Subtitle
|
|
target model.Subtitle
|
|
verify func(t *testing.T, result model.Subtitle)
|
|
}{
|
|
{
|
|
name: "Equal entry count",
|
|
source: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Title = "Source Title"
|
|
sub.Entries = []model.SubtitleEntry{
|
|
{
|
|
Index: 1,
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Text: "Source line one.",
|
|
},
|
|
{
|
|
Index: 2,
|
|
StartTime: model.Timestamp{Seconds: 5},
|
|
EndTime: model.Timestamp{Seconds: 8},
|
|
Text: "Source line two.",
|
|
},
|
|
}
|
|
return sub
|
|
}(),
|
|
target: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Title = "Target Title"
|
|
sub.Metadata = map[string]string{"WEBVTT": "Some Styles"}
|
|
sub.Entries = []model.SubtitleEntry{
|
|
{
|
|
Index: 1,
|
|
StartTime: model.Timestamp{Minutes: 1},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 3},
|
|
Text: "Target line one.",
|
|
Styles: map[string]string{"align": "start", "position": "10%"},
|
|
},
|
|
{
|
|
Index: 2,
|
|
StartTime: model.Timestamp{Minutes: 1, Seconds: 5},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 8},
|
|
Text: "Target line two.",
|
|
Styles: map[string]string{"align": "middle"},
|
|
},
|
|
}
|
|
return sub
|
|
}(),
|
|
verify: func(t *testing.T, result model.Subtitle) {
|
|
if result.Format != "vtt" {
|
|
t.Errorf("Expected format 'vtt', got '%s'", result.Format)
|
|
}
|
|
|
|
if result.Title != "Target Title" {
|
|
t.Errorf("Expected title 'Target Title', got '%s'", result.Title)
|
|
}
|
|
|
|
if len(result.Metadata) == 0 || result.Metadata["WEBVTT"] != "Some Styles" {
|
|
t.Errorf("Expected to preserve metadata, got %v", result.Metadata)
|
|
}
|
|
|
|
if len(result.Entries) != 2 {
|
|
t.Errorf("Expected 2 entries, got %d", len(result.Entries))
|
|
return
|
|
}
|
|
|
|
// Check that first entry has source timing
|
|
if result.Entries[0].StartTime.Seconds != 1 || result.Entries[0].EndTime.Seconds != 4 {
|
|
t.Errorf("First entry timing incorrect, got start: %+v, end: %+v",
|
|
result.Entries[0].StartTime, result.Entries[0].EndTime)
|
|
}
|
|
|
|
// Check that styles are preserved
|
|
if result.Entries[0].Styles["align"] != "start" || result.Entries[0].Styles["position"] != "10%" {
|
|
t.Errorf("Expected to preserve styles, got %v", result.Entries[0].Styles)
|
|
}
|
|
|
|
// Check text is preserved
|
|
if result.Entries[0].Text != "Target line one." {
|
|
t.Errorf("Expected target text, got '%s'", result.Entries[0].Text)
|
|
}
|
|
|
|
// Check indexes are sequential
|
|
if result.Entries[0].Index != 1 || result.Entries[1].Index != 2 {
|
|
t.Errorf("Expected sequential indexes 1, 2, got %d, %d",
|
|
result.Entries[0].Index, result.Entries[1].Index)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Different entry count - more source entries",
|
|
source: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Entries = []model.SubtitleEntry{
|
|
{
|
|
Index: 1,
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Text: "Source line one.",
|
|
},
|
|
{
|
|
Index: 2,
|
|
StartTime: model.Timestamp{Seconds: 5},
|
|
EndTime: model.Timestamp{Seconds: 8},
|
|
Text: "Source line two.",
|
|
},
|
|
{
|
|
Index: 3,
|
|
StartTime: model.Timestamp{Seconds: 9},
|
|
EndTime: model.Timestamp{Seconds: 12},
|
|
Text: "Source line three.",
|
|
},
|
|
}
|
|
return sub
|
|
}(),
|
|
target: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Entries = []model.SubtitleEntry{
|
|
{
|
|
Index: 1,
|
|
StartTime: model.Timestamp{Minutes: 1},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 3},
|
|
Text: "Target line one.",
|
|
},
|
|
{
|
|
Index: 2,
|
|
StartTime: model.Timestamp{Minutes: 1, Seconds: 5},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 8},
|
|
Text: "Target line two.",
|
|
},
|
|
}
|
|
return sub
|
|
}(),
|
|
verify: func(t *testing.T, result model.Subtitle) {
|
|
if len(result.Entries) != 2 {
|
|
t.Errorf("Expected 2 entries, got %d", len(result.Entries))
|
|
return
|
|
}
|
|
|
|
// Check scaling - first entry should get timing from first source
|
|
if result.Entries[0].StartTime.Seconds != 1 {
|
|
t.Errorf("First entry start time incorrect, got %+v", result.Entries[0].StartTime)
|
|
}
|
|
|
|
// Second entry should have timing from last source entry due to scaling
|
|
if result.Entries[1].StartTime.Seconds != 9 {
|
|
t.Errorf("Second entry start time incorrect, expected scaled timing, got %+v",
|
|
result.Entries[1].StartTime)
|
|
}
|
|
|
|
// Check target text preserved
|
|
if result.Entries[0].Text != "Target line one." || result.Entries[1].Text != "Target line two." {
|
|
t.Errorf("Expected target text to be preserved")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Empty source entries",
|
|
source: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Entries = []model.SubtitleEntry{}
|
|
return sub
|
|
}(),
|
|
target: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Title = "Target Title"
|
|
sub.Entries = []model.SubtitleEntry{
|
|
{
|
|
Index: 1,
|
|
StartTime: model.Timestamp{Minutes: 1},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 3},
|
|
Text: "Target line one.",
|
|
Styles: map[string]string{"align": "start"},
|
|
},
|
|
}
|
|
return sub
|
|
}(),
|
|
verify: func(t *testing.T, result model.Subtitle) {
|
|
if len(result.Entries) != 1 {
|
|
t.Errorf("Expected 1 entry, got %d", len(result.Entries))
|
|
return
|
|
}
|
|
|
|
// With empty source, target timing should be preserved
|
|
if result.Entries[0].StartTime.Minutes != 1 || result.Entries[0].EndTime.Minutes != 1 {
|
|
t.Errorf("Empty source should preserve target timing, got start: %+v, end: %+v",
|
|
result.Entries[0].StartTime, result.Entries[0].EndTime)
|
|
}
|
|
|
|
// Check target styles preserved
|
|
if _, hasAlign := result.Entries[0].Styles["align"]; !hasAlign || result.Entries[0].Styles["align"] != "start" {
|
|
t.Errorf("Expected target styles to be preserved, got %v", result.Entries[0].Styles)
|
|
}
|
|
|
|
// Check title is preserved
|
|
if result.Title != "Target Title" {
|
|
t.Errorf("Expected target title to be preserved")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Empty target entries",
|
|
source: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Entries = []model.SubtitleEntry{
|
|
{
|
|
Index: 1,
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Text: "Source line one.",
|
|
},
|
|
}
|
|
return sub
|
|
}(),
|
|
target: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Title = "Target Title"
|
|
sub.Entries = []model.SubtitleEntry{}
|
|
return sub
|
|
}(),
|
|
verify: func(t *testing.T, result model.Subtitle) {
|
|
if len(result.Entries) != 0 {
|
|
t.Errorf("Expected 0 entries, got %d", len(result.Entries))
|
|
return
|
|
}
|
|
|
|
// Should keep target metadata
|
|
if result.Title != "Target Title" {
|
|
t.Errorf("Expected target title to be preserved")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Different entry count - more target entries",
|
|
source: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Entries = []model.SubtitleEntry{
|
|
{
|
|
Index: 1,
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Text: "Source line one.",
|
|
},
|
|
}
|
|
return sub
|
|
}(),
|
|
target: func() model.Subtitle {
|
|
sub := model.NewSubtitle()
|
|
sub.Format = "vtt"
|
|
sub.Entries = []model.SubtitleEntry{
|
|
{
|
|
Index: 1,
|
|
StartTime: model.Timestamp{Minutes: 1},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 3},
|
|
Text: "Target line one.",
|
|
},
|
|
{
|
|
Index: 2,
|
|
StartTime: model.Timestamp{Minutes: 1, Seconds: 5},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 8},
|
|
Text: "Target line two.",
|
|
},
|
|
{
|
|
Index: 3,
|
|
StartTime: model.Timestamp{Minutes: 1, Seconds: 10},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 13},
|
|
Text: "Target line three.",
|
|
},
|
|
}
|
|
return sub
|
|
}(),
|
|
verify: func(t *testing.T, result model.Subtitle) {
|
|
if len(result.Entries) != 3 {
|
|
t.Errorf("Expected 3 entries, got %d", len(result.Entries))
|
|
return
|
|
}
|
|
|
|
// Check that first entry has source timing
|
|
if result.Entries[0].StartTime.Seconds != 1 {
|
|
t.Errorf("First entry start time incorrect, got %+v", result.Entries[0].StartTime)
|
|
}
|
|
|
|
// The other entries should be scaled from the source
|
|
// With only one source entry, all target entries should get the same start time
|
|
if result.Entries[1].StartTime.Seconds != 1 || result.Entries[2].StartTime.Seconds != 1 {
|
|
t.Errorf("All entries should have same timing with only one source entry, got: %+v, %+v",
|
|
result.Entries[1].StartTime, result.Entries[2].StartTime)
|
|
}
|
|
|
|
// Check indexes are sequential
|
|
if result.Entries[0].Index != 1 || result.Entries[1].Index != 2 || result.Entries[2].Index != 3 {
|
|
t.Errorf("Expected sequential indexes 1, 2, 3, got %d, %d, %d",
|
|
result.Entries[0].Index, result.Entries[1].Index, result.Entries[2].Index)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := syncVTTTimeline(tc.source, tc.target)
|
|
|
|
if tc.verify != nil {
|
|
tc.verify(t, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSyncSRTTimeline(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
sourceEntries []model.SRTEntry
|
|
targetEntries []model.SRTEntry
|
|
verify func(t *testing.T, result []model.SRTEntry)
|
|
}{
|
|
{
|
|
name: "Equal entry count",
|
|
sourceEntries: []model.SRTEntry{
|
|
{
|
|
Number: 1,
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Content: "Source line one.",
|
|
},
|
|
{
|
|
Number: 2,
|
|
StartTime: model.Timestamp{Seconds: 5},
|
|
EndTime: model.Timestamp{Seconds: 8},
|
|
Content: "Source line two.",
|
|
},
|
|
},
|
|
targetEntries: []model.SRTEntry{
|
|
{
|
|
Number: 1,
|
|
StartTime: model.Timestamp{Minutes: 1},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 3},
|
|
Content: "Target line one.",
|
|
},
|
|
{
|
|
Number: 2,
|
|
StartTime: model.Timestamp{Minutes: 1, Seconds: 5},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 8},
|
|
Content: "Target line two.",
|
|
},
|
|
},
|
|
verify: func(t *testing.T, result []model.SRTEntry) {
|
|
if len(result) != 2 {
|
|
t.Errorf("Expected 2 entries, got %d", len(result))
|
|
return
|
|
}
|
|
|
|
// Check first entry
|
|
if result[0].StartTime.Seconds != 1 || result[0].EndTime.Seconds != 4 {
|
|
t.Errorf("First entry timing incorrect, got start: %+v, end: %+v",
|
|
result[0].StartTime, result[0].EndTime)
|
|
}
|
|
if result[0].Content != "Target line one." {
|
|
t.Errorf("Expected content 'Target line one.', got '%s'", result[0].Content)
|
|
}
|
|
if result[0].Number != 1 {
|
|
t.Errorf("Expected entry number 1, got %d", result[0].Number)
|
|
}
|
|
|
|
// Check second entry
|
|
if result[1].StartTime.Seconds != 5 || result[1].EndTime.Seconds != 8 {
|
|
t.Errorf("Second entry timing incorrect, got start: %+v, end: %+v",
|
|
result[1].StartTime, result[1].EndTime)
|
|
}
|
|
if result[1].Content != "Target line two." {
|
|
t.Errorf("Expected content 'Target line two.', got '%s'", result[1].Content)
|
|
}
|
|
if result[1].Number != 2 {
|
|
t.Errorf("Expected entry number 2, got %d", result[1].Number)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Different entry count - more source entries",
|
|
sourceEntries: []model.SRTEntry{
|
|
{
|
|
Number: 1,
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Content: "Source line one.",
|
|
},
|
|
{
|
|
Number: 2,
|
|
StartTime: model.Timestamp{Seconds: 5},
|
|
EndTime: model.Timestamp{Seconds: 8},
|
|
Content: "Source line two.",
|
|
},
|
|
{
|
|
Number: 3,
|
|
StartTime: model.Timestamp{Seconds: 9},
|
|
EndTime: model.Timestamp{Seconds: 12},
|
|
Content: "Source line three.",
|
|
},
|
|
},
|
|
targetEntries: []model.SRTEntry{
|
|
{
|
|
Number: 1,
|
|
StartTime: model.Timestamp{Minutes: 1},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 3},
|
|
Content: "Target line one.",
|
|
},
|
|
{
|
|
Number: 2,
|
|
StartTime: model.Timestamp{Minutes: 1, Seconds: 5},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 8},
|
|
Content: "Target line two.",
|
|
},
|
|
},
|
|
verify: func(t *testing.T, result []model.SRTEntry) {
|
|
if len(result) != 2 {
|
|
t.Errorf("Expected 2 entries, got %d", len(result))
|
|
return
|
|
}
|
|
|
|
// First entry should have timing from first source entry
|
|
if result[0].StartTime.Seconds != 1 {
|
|
t.Errorf("First entry start time incorrect, got %+v", result[0].StartTime)
|
|
}
|
|
|
|
// Second entry should have scaling from source entry 3 (at index 2)
|
|
if result[1].StartTime.Seconds != 9 {
|
|
t.Errorf("Second entry start time incorrect, got %+v", result[1].StartTime)
|
|
}
|
|
|
|
// Check content content preserved
|
|
if result[0].Content != "Target line one." || result[1].Content != "Target line two." {
|
|
t.Errorf("Expected target content to be preserved")
|
|
}
|
|
|
|
// Check numbering
|
|
if result[0].Number != 1 || result[1].Number != 2 {
|
|
t.Errorf("Expected sequential numbering 1, 2, got %d, %d",
|
|
result[0].Number, result[1].Number)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Empty source entries",
|
|
sourceEntries: []model.SRTEntry{},
|
|
targetEntries: []model.SRTEntry{
|
|
{
|
|
Number: 1,
|
|
StartTime: model.Timestamp{Minutes: 1},
|
|
EndTime: model.Timestamp{Minutes: 1, Seconds: 3},
|
|
Content: "Target line one.",
|
|
},
|
|
},
|
|
verify: func(t *testing.T, result []model.SRTEntry) {
|
|
if len(result) != 1 {
|
|
t.Errorf("Expected 1 entry, got %d", len(result))
|
|
return
|
|
}
|
|
|
|
// With empty source, target timing should be preserved
|
|
if result[0].StartTime.Minutes != 1 || result[0].EndTime.Minutes != 1 ||
|
|
result[0].EndTime.Seconds != 3 {
|
|
t.Errorf("Expected target timing to be preserved with empty source")
|
|
}
|
|
|
|
// Check content is preserved
|
|
if result[0].Content != "Target line one." {
|
|
t.Errorf("Expected target content to be preserved")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Empty target entries",
|
|
sourceEntries: []model.SRTEntry{
|
|
{
|
|
Number: 1,
|
|
StartTime: model.Timestamp{Seconds: 1},
|
|
EndTime: model.Timestamp{Seconds: 4},
|
|
Content: "Source line one.",
|
|
},
|
|
},
|
|
targetEntries: []model.SRTEntry{},
|
|
verify: func(t *testing.T, result []model.SRTEntry) {
|
|
if len(result) != 0 {
|
|
t.Errorf("Expected 0 entries, got %d", len(result))
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := syncSRTTimeline(tc.sourceEntries, tc.targetEntries)
|
|
|
|
if tc.verify != nil {
|
|
tc.verify(t, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCalculateDuration(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
start model.Timestamp
|
|
end model.Timestamp
|
|
expected model.Timestamp
|
|
}{
|
|
{
|
|
name: "Simple duration",
|
|
start: model.Timestamp{Minutes: 1, Seconds: 30},
|
|
end: model.Timestamp{Minutes: 3, Seconds: 10},
|
|
expected: model.Timestamp{Minutes: 1, Seconds: 40},
|
|
},
|
|
{
|
|
name: "Duration with hours",
|
|
start: model.Timestamp{Hours: 1, Minutes: 20},
|
|
end: model.Timestamp{Hours: 2, Minutes: 10},
|
|
expected: model.Timestamp{Hours: 0, Minutes: 50},
|
|
},
|
|
{
|
|
name: "Duration with milliseconds",
|
|
start: model.Timestamp{Seconds: 10, Milliseconds: 500},
|
|
end: model.Timestamp{Seconds: 20, Milliseconds: 800},
|
|
expected: model.Timestamp{Seconds: 10, Milliseconds: 300},
|
|
},
|
|
{
|
|
name: "End before start (should return zero)",
|
|
start: model.Timestamp{Minutes: 5},
|
|
end: model.Timestamp{Minutes: 3},
|
|
expected: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 0, Milliseconds: 0},
|
|
},
|
|
{
|
|
name: "Complex duration with carry",
|
|
start: model.Timestamp{Hours: 1, Minutes: 45, Seconds: 30, Milliseconds: 500},
|
|
end: model.Timestamp{Hours: 3, Minutes: 20, Seconds: 15, Milliseconds: 800},
|
|
expected: model.Timestamp{Hours: 1, Minutes: 34, Seconds: 45, Milliseconds: 300},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := calculateDuration(tc.start, tc.end)
|
|
|
|
if result.Hours != tc.expected.Hours ||
|
|
result.Minutes != tc.expected.Minutes ||
|
|
result.Seconds != tc.expected.Seconds ||
|
|
result.Milliseconds != tc.expected.Milliseconds {
|
|
t.Errorf("Expected %+v, got %+v", tc.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddDuration(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
start model.Timestamp
|
|
duration model.Timestamp
|
|
expected model.Timestamp
|
|
}{
|
|
{
|
|
name: "Simple addition",
|
|
start: model.Timestamp{Minutes: 1, Seconds: 30},
|
|
duration: model.Timestamp{Minutes: 2, Seconds: 15},
|
|
expected: model.Timestamp{Minutes: 3, Seconds: 45},
|
|
},
|
|
{
|
|
name: "Addition with carry",
|
|
start: model.Timestamp{Minutes: 58, Seconds: 45},
|
|
duration: model.Timestamp{Minutes: 4, Seconds: 30},
|
|
expected: model.Timestamp{Hours: 1, Minutes: 3, Seconds: 15},
|
|
},
|
|
{
|
|
name: "Addition with milliseconds",
|
|
start: model.Timestamp{Seconds: 10, Milliseconds: 500},
|
|
duration: model.Timestamp{Seconds: 5, Milliseconds: 800},
|
|
expected: model.Timestamp{Seconds: 16, Milliseconds: 300},
|
|
},
|
|
{
|
|
name: "Zero duration",
|
|
start: model.Timestamp{Minutes: 5, Seconds: 30},
|
|
duration: model.Timestamp{},
|
|
expected: model.Timestamp{Minutes: 5, Seconds: 30},
|
|
},
|
|
{
|
|
name: "Complex addition with multiple carries",
|
|
start: model.Timestamp{Hours: 1, Minutes: 59, Seconds: 59, Milliseconds: 900},
|
|
duration: model.Timestamp{Hours: 2, Minutes: 10, Seconds: 15, Milliseconds: 200},
|
|
expected: model.Timestamp{Hours: 4, Minutes: 10, Seconds: 15, Milliseconds: 100},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := addDuration(tc.start, tc.duration)
|
|
|
|
if result.Hours != tc.expected.Hours ||
|
|
result.Minutes != tc.expected.Minutes ||
|
|
result.Seconds != tc.expected.Seconds ||
|
|
result.Milliseconds != tc.expected.Milliseconds {
|
|
t.Errorf("Expected %+v, got %+v", tc.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|