package srt import ( "os" "path/filepath" "strings" "testing" "sub-cli/internal/model" ) func TestParse(t *testing.T) { // Create a temporary test file content := `1 00:00:01,000 --> 00:00:04,000 This is the first line. 2 00:00:05,000 --> 00:00:08,000 This is the second line. 3 00:00:09,500 --> 00:00:12,800 This is the third line with a line break. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.srt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Test parsing entries, err := Parse(testFile) if err != nil { t.Fatalf("Parse failed: %v", err) } // Verify results if len(entries) != 3 { t.Errorf("Expected 3 entries, got %d", len(entries)) } // Check first entry if entries[0].Number != 1 { t.Errorf("First entry number: expected 1, got %d", entries[0].Number) } if entries[0].StartTime.Hours != 0 || entries[0].StartTime.Minutes != 0 || entries[0].StartTime.Seconds != 1 || entries[0].StartTime.Milliseconds != 0 { t.Errorf("First entry start time: expected 00:00:01,000, got %+v", entries[0].StartTime) } if entries[0].EndTime.Hours != 0 || entries[0].EndTime.Minutes != 0 || entries[0].EndTime.Seconds != 4 || entries[0].EndTime.Milliseconds != 0 { t.Errorf("First entry end time: expected 00:00:04,000, got %+v", entries[0].EndTime) } if entries[0].Content != "This is the first line." { t.Errorf("First entry content: expected 'This is the first line.', got '%s'", entries[0].Content) } // Check third entry if entries[2].Number != 3 { t.Errorf("Third entry number: expected 3, got %d", entries[2].Number) } expectedContent := "This is the third line\nwith a line break." if entries[2].Content != expectedContent { t.Errorf("Third entry content: expected '%s', got '%s'", expectedContent, entries[2].Content) } } func TestGenerate(t *testing.T) { // Create test entries entries := []model.SRTEntry{ { Number: 1, StartTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, EndTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0}, Content: "This is the first line.", }, { Number: 2, StartTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 5, Milliseconds: 0}, EndTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 8, Milliseconds: 0}, Content: "This is the second line.", }, } // Generate SRT file tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "output.srt") err := Generate(entries, outputFile) if err != nil { t.Fatalf("Generate failed: %v", err) } // Verify generated content content, err := os.ReadFile(outputFile) if err != nil { t.Fatalf("Failed to read output file: %v", err) } // Check content lines := strings.Split(string(content), "\n") if len(lines) < 6 { t.Fatalf("Expected at least 6 lines, got %d", len(lines)) } if lines[0] != "1" { t.Errorf("Expected first line to be '1', got '%s'", lines[0]) } if lines[1] != "00:00:01,000 --> 00:00:04,000" { t.Errorf("Expected second line to be time range, got '%s'", lines[1]) } if lines[2] != "This is the first line." { t.Errorf("Expected third line to be content, got '%s'", lines[2]) } } func TestConvertToSubtitle(t *testing.T) { // Create a temporary test file content := `1 00:00:01,000 --> 00:00:04,000 This is the first line. 2 00:00:05,000 --> 00:00:08,000 This is the second line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.srt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Convert to subtitle subtitle, err := ConvertToSubtitle(testFile) if err != nil { t.Fatalf("ConvertToSubtitle failed: %v", err) } // Check result if subtitle.Format != "srt" { t.Errorf("Expected format 'srt', got '%s'", subtitle.Format) } if len(subtitle.Entries) != 2 { t.Errorf("Expected 2 entries, got %d", len(subtitle.Entries)) } // Check first entry if subtitle.Entries[0].Text != "This is the first line." { t.Errorf("Expected first entry text 'This is the first line.', got '%s'", subtitle.Entries[0].Text) } } func TestConvertFromSubtitle(t *testing.T) { // Create test subtitle subtitle := model.NewSubtitle() subtitle.Format = "srt" entry1 := model.NewSubtitleEntry() entry1.Index = 1 entry1.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0} entry1.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0} entry1.Text = "This is the first line." entry2 := model.NewSubtitleEntry() entry2.Index = 2 entry2.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 5, Milliseconds: 0} entry2.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 8, Milliseconds: 0} entry2.Text = "This is the second line." subtitle.Entries = append(subtitle.Entries, entry1, entry2) // Convert from subtitle to SRT tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "output.srt") err := ConvertFromSubtitle(subtitle, outputFile) if err != nil { t.Fatalf("ConvertFromSubtitle failed: %v", err) } // Verify by parsing back entries, err := Parse(outputFile) if err != nil { t.Fatalf("Failed to parse output file: %v", err) } if len(entries) != 2 { t.Errorf("Expected 2 entries, got %d", len(entries)) } if entries[0].Content != "This is the first line." { t.Errorf("Expected first entry content 'This is the first line.', got '%s'", entries[0].Content) } } func TestFormat(t *testing.T) { // Create test file with non-sequential numbers content := `2 00:00:01,000 --> 00:00:04,000 This is the first line. 5 00:00:05,000 --> 00:00:08,000 This is the second line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.srt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Format the file err := Format(testFile) if err != nil { t.Fatalf("Format failed: %v", err) } // Verify by parsing back entries, err := Parse(testFile) if err != nil { t.Fatalf("Failed to parse formatted file: %v", err) } // Check that numbers are sequential if entries[0].Number != 1 { t.Errorf("Expected first entry number to be 1, got %d", entries[0].Number) } if entries[1].Number != 2 { t.Errorf("Expected second entry number to be 2, got %d", entries[1].Number) } } func TestParseSRTTimestamp(t *testing.T) { testCases := []struct { name string input string expected model.Timestamp }{ { name: "Standard format", input: "00:00:01,000", expected: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, }, { name: "With milliseconds", input: "00:00:01,500", expected: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 500}, }, { name: "Full hours, minutes, seconds", input: "01:02:03,456", expected: model.Timestamp{Hours: 1, Minutes: 2, Seconds: 3, Milliseconds: 456}, }, { name: "With dot instead of comma", input: "00:00:01.000", // Should auto-convert . to , expected: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, }, { name: "Invalid format", input: "invalid", expected: model.Timestamp{}, // Should return zero timestamp }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := parseSRTTimestamp(tc.input) if result != tc.expected { t.Errorf("For input '%s', expected %+v, got %+v", tc.input, tc.expected, result) } }) } } func TestFormatSRTTimestamp(t *testing.T) { testCases := []struct { name string input model.Timestamp expected string }{ { name: "Zero timestamp", input: model.Timestamp{}, expected: "00:00:00,000", }, { name: "Simple seconds", input: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, expected: "00:00:01,000", }, { name: "With milliseconds", input: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 500}, expected: "00:00:01,500", }, { name: "Full timestamp", input: model.Timestamp{Hours: 1, Minutes: 2, Seconds: 3, Milliseconds: 456}, expected: "01:02:03,456", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := formatSRTTimestamp(tc.input) if result != tc.expected { t.Errorf("For timestamp %+v, expected '%s', got '%s'", tc.input, tc.expected, result) } }) } } func TestIsEntryTimeStampUnset(t *testing.T) { testCases := []struct { name string entry model.SRTEntry expected bool }{ { name: "Unset timestamp", entry: model.SRTEntry{Number: 1}, expected: true, }, { name: "Set timestamp", entry: model.SRTEntry{ Number: 1, StartTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, }, expected: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := isEntryTimeStampUnset(tc.entry) if result != tc.expected { t.Errorf("For entry %+v, expected isEntryTimeStampUnset to be %v, got %v", tc.entry, tc.expected, result) } }) } } func TestConvertToLyrics(t *testing.T) { // Create test entries entries := []model.SRTEntry{ { Number: 1, StartTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, EndTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0}, Content: "This is the first line.", }, { Number: 2, StartTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 5, Milliseconds: 0}, EndTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 8, Milliseconds: 0}, Content: "This is the second line.", }, } // Convert to lyrics lyrics := ConvertToLyrics(entries) // Check result if len(lyrics.Timeline) != 2 { t.Errorf("Expected 2 timeline entries, got %d", len(lyrics.Timeline)) } if len(lyrics.Content) != 2 { t.Errorf("Expected 2 content entries, got %d", len(lyrics.Content)) } // Check timeline entries if lyrics.Timeline[0] != entries[0].StartTime { t.Errorf("First timeline entry: expected %+v, got %+v", entries[0].StartTime, lyrics.Timeline[0]) } if lyrics.Timeline[1] != entries[1].StartTime { t.Errorf("Second timeline entry: expected %+v, got %+v", entries[1].StartTime, lyrics.Timeline[1]) } // Check content entries if lyrics.Content[0] != entries[0].Content { t.Errorf("First content entry: expected '%s', got '%s'", entries[0].Content, lyrics.Content[0]) } if lyrics.Content[1] != entries[1].Content { t.Errorf("Second content entry: expected '%s', got '%s'", entries[1].Content, lyrics.Content[1]) } } func TestParse_EdgeCases(t *testing.T) { // Test with empty file tempDir := t.TempDir() emptyFile := filepath.Join(tempDir, "empty.srt") if err := os.WriteFile(emptyFile, []byte{}, 0644); err != nil { t.Fatalf("Failed to create empty test file: %v", err) } entries, err := Parse(emptyFile) if err != nil { t.Fatalf("Parse failed on empty file: %v", err) } if len(entries) != 0 { t.Errorf("Expected 0 entries for empty file, got %d", len(entries)) } // Test with malformed file (missing timestamp line) malformedFile := filepath.Join(tempDir, "malformed.srt") content := `1 This is missing a timestamp line. 2 00:00:05,000 --> 00:00:08,000 This is valid. ` if err := os.WriteFile(malformedFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create malformed test file: %v", err) } entries, err = Parse(malformedFile) if err != nil { t.Fatalf("Parse failed on malformed file: %v", err) } // SRT解析器更宽容,可能会解析出两个条目 if len(entries) != 2 { t.Errorf("Expected 2 entries, got %d", len(entries)) } // Test with incomplete last entry incompleteFile := filepath.Join(tempDir, "incomplete.srt") content = `1 00:00:01,000 --> 00:00:04,000 This is complete. 2 00:00:05,000 --> 00:00:08,000 ` if err := os.WriteFile(incompleteFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create incomplete test file: %v", err) } entries, err = Parse(incompleteFile) if err != nil { t.Fatalf("Parse failed on incomplete file: %v", err) } // Should have one complete entry, the incomplete one is discarded due to empty content if len(entries) != 1 { t.Errorf("Expected 1 entry (only the completed one), got %d", len(entries)) } } func TestParse_FileError(t *testing.T) { // Test with non-existent file _, err := Parse("/nonexistent/file.srt") if err == nil { t.Error("Expected error when parsing non-existent file, got nil") } } func TestGenerate_FileError(t *testing.T) { // Create test entries entries := []model.SRTEntry{ { Number: 1, StartTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, EndTime: model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0}, Content: "This is a test line.", }, } // Test with invalid path err := Generate(entries, "/nonexistent/directory/file.srt") if err == nil { t.Error("Expected error when generating to invalid path, got nil") } } func TestFormat_FileError(t *testing.T) { // Test with non-existent file err := Format("/nonexistent/file.srt") if err == nil { t.Error("Expected error when formatting non-existent file, got nil") } } func TestConvertToSubtitle_WithHTMLTags(t *testing.T) { // Create a temporary test file with HTML tags content := `1 00:00:01,000 --> 00:00:04,000 This is in italic. 2 00:00:05,000 --> 00:00:08,000 This is in bold. 3 00:00:09,000 --> 00:00:12,000 This is underlined. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "styles.srt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file with HTML tags: %v", err) } // Convert to subtitle subtitle, err := ConvertToSubtitle(testFile) if err != nil { t.Fatalf("ConvertToSubtitle failed: %v", err) } // Check if HTML tags were detected if value, ok := subtitle.Entries[0].FormatData["has_html_tags"]; !ok || value != true { t.Errorf("Expected FormatData to contain has_html_tags=true for entry with italic") } if value, ok := subtitle.Entries[0].Styles["italic"]; !ok || value != "true" { t.Errorf("Expected Styles to contain italic=true for entry with tag") } if value, ok := subtitle.Entries[1].Styles["bold"]; !ok || value != "true" { t.Errorf("Expected Styles to contain bold=true for entry with tag") } if value, ok := subtitle.Entries[2].Styles["underline"]; !ok || value != "true" { t.Errorf("Expected Styles to contain underline=true for entry with tag") } } func TestConvertToSubtitle_FileError(t *testing.T) { // Test with non-existent file _, err := ConvertToSubtitle("/nonexistent/file.srt") if err == nil { t.Error("Expected error when converting non-existent file, got nil") } } func TestConvertFromSubtitle_WithStyling(t *testing.T) { // Create a subtitle with style attributes subtitle := model.NewSubtitle() subtitle.Format = "srt" // Create an entry with italics entry1 := model.NewSubtitleEntry() entry1.Index = 1 entry1.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0} entry1.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0} entry1.Text = "This should be italic." entry1.Styles["italic"] = "true" // Create an entry with bold entry2 := model.NewSubtitleEntry() entry2.Index = 2 entry2.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 5, Milliseconds: 0} entry2.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 8, Milliseconds: 0} entry2.Text = "This should be bold." entry2.Styles["bold"] = "true" // Create an entry with underline entry3 := model.NewSubtitleEntry() entry3.Index = 3 entry3.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 9, Milliseconds: 0} entry3.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 12, Milliseconds: 0} entry3.Text = "This should be underlined." entry3.Styles["underline"] = "true" subtitle.Entries = append(subtitle.Entries, entry1, entry2, entry3) // Convert from subtitle to SRT tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "styled.srt") err := ConvertFromSubtitle(subtitle, outputFile) if err != nil { t.Fatalf("ConvertFromSubtitle failed: %v", err) } // Verify by reading the file directly content, err := os.ReadFile(outputFile) if err != nil { t.Fatalf("Failed to read output file: %v", err) } // Check that HTML tags were applied contentStr := string(content) if !strings.Contains(contentStr, "This should be italic.") { t.Errorf("Expected italic HTML tags to be applied") } if !strings.Contains(contentStr, "This should be bold.") { t.Errorf("Expected bold HTML tags to be applied") } if !strings.Contains(contentStr, "This should be underlined.") { t.Errorf("Expected underline HTML tags to be applied") } } func TestConvertFromSubtitle_FileError(t *testing.T) { // Create simple subtitle subtitle := model.NewSubtitle() subtitle.Entries = append(subtitle.Entries, model.NewSubtitleEntry()) // Test with invalid path err := ConvertFromSubtitle(subtitle, "/nonexistent/directory/file.srt") if err == nil { t.Error("Expected error when converting to invalid path, got nil") } } func TestConvertFromSubtitle_WithExistingHTMLTags(t *testing.T) { // Create a subtitle with text that already contains HTML tags subtitle := model.NewSubtitle() subtitle.Format = "srt" // Create an entry with existing italic tags but also style attribute entry := model.NewSubtitleEntry() entry.Index = 1 entry.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0} entry.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0} entry.Text = "Already italic text." entry.Styles["italic"] = "true" // Should not double-wrap with tags subtitle.Entries = append(subtitle.Entries, entry) // Convert from subtitle to SRT tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "existing_tags.srt") err := ConvertFromSubtitle(subtitle, outputFile) if err != nil { t.Fatalf("ConvertFromSubtitle failed: %v", err) } // Verify by reading the file directly content, err := os.ReadFile(outputFile) if err != nil { t.Fatalf("Failed to read output file: %v", err) } // Should not have double tags contentStr := string(content) if strings.Contains(contentStr, "") { t.Errorf("Expected no duplicate italic tags, but found them") } }