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