Files
djk/internal/app/ui_merge_jobs_test.go
T

111 lines
3.8 KiB
Go

package app
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"time"
)
func TestMergeJobsSurviveRequestCancellationAndDeduplicate(t *testing.T) {
for _, async := range []bool{false, true} {
t.Run(map[bool]string{false: "curl-compatible", true: "async-page"}[async], func(t *testing.T) {
app, session := nativePlaybackFixture(t, "2", "320x180")
app.taskOrder = session.downloadIDs
app.statePath = filepath.Join(app.cfg.dataDirectory(), "ui-state.json")
app.downloader.ffmpegInstaller = &ffmpegInstaller{state: ffmpegInstallState{Status: "ready", Path: app.downloader.cfg.FFmpeg}}
app.cfg.SkipBytes = 1
app.mergeMu.Lock()
var once sync.Once
unlock := func() { once.Do(app.mergeMu.Unlock) }
defer func() { unlock(); app.stopMergeJobs() }()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
body, _ := json.Marshal(map[string]any{"dramaIds": []string{historyFixtureDramaID}, "async": async})
request := httptest.NewRequest(http.MethodPost, "/api/ui/merge", bytes.NewReader(body)).WithContext(ctx)
writer := httptest.NewRecorder()
returned := make(chan struct{})
go func() { app.handleMerge(writer, request); close(returned) }()
var job *uiMergeJob
deadline := time.Now().Add(5 * time.Second)
for job == nil && time.Now().Before(deadline) {
app.mu.Lock()
job = app.mergeJobs[historyFixtureDramaID]
app.mu.Unlock()
time.Sleep(time.Millisecond)
}
if job == nil {
t.Fatal("merge was not queued")
}
cancel()
select {
case <-returned:
case <-time.After(time.Second):
t.Fatal("canceled request remained tied to the merge")
}
if async && writer.Code != http.StatusAccepted || job.ctx.Err() != nil {
t.Fatal("request canceled its background merge", writer.Code, job.ctx.Err())
}
app.mu.Lock()
duplicate, err := app.enqueueMergesLocked([]string{historyFixtureDramaID}, true)
app.mu.Unlock()
if err != nil || len(duplicate) != 1 || duplicate[0] != job || job.remove {
t.Fatal("duplicate submission changed or duplicated the original merge", err)
}
unlock()
select {
case <-job.done:
case <-time.After(30 * time.Second):
t.Fatal("detached merge did not finish")
}
if !job.result.OK {
t.Fatal("background merge failed", job.result.Error)
}
if _, err := os.Stat(job.result.OutputPath); err != nil {
t.Fatal("background output missing", err)
}
if _, err := os.Stat(session.tasks[0].OutPath); err != nil {
t.Fatal("duplicate submission deleted original episodes", err)
}
})
}
}
func TestMergeQueueCancelAndRestartAreExplicit(t *testing.T) {
app := viewerTestApp(t)
app.statePath = filepath.Join(app.cfg.dataDirectory(), "ui-state.json")
app.mergeMu.Lock()
var once sync.Once
unlock := func() { once.Do(app.mergeMu.Unlock) }
defer func() { unlock(); app.stopMergeJobs() }()
app.mu.Lock()
jobs, err := app.enqueueMergesLocked([]string{historyFixtureDramaID}, false)
app.mu.Unlock()
if err != nil {
t.Fatal(err)
}
restored := &UIApp{cfg: app.cfg, statePath: app.statePath, tasks: map[string]*UITask{}}
restored.loadState()
if state := restored.merges[historyFixtureDramaID]; state == nil || state.Status != "failed" || state.Error == "" {
t.Fatal("restart left an interrupted merge permanently queued", state)
}
writer := historyJSONRequest(t, app, app.handleMergeCancel, "/api/ui/merge/cancel", map[string]any{"dramaIds": []string{historyFixtureDramaID}})
if writer.Code != http.StatusOK {
t.Fatal(writer.Code, writer.Body.String())
}
select {
case <-jobs[0].done:
case <-time.After(time.Second):
t.Fatal("queued cancellation waited for an unrelated active merge")
}
if jobs[0].result.OK || app.merges[historyFixtureDramaID].Status != "failed" {
t.Fatal("canceled merge reported success")
}
}