mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-08-14 17:03:57 +08:00
* feat: show upload progress for file tasks * feat: show upload progress for batch file tasks * style: distinguish download and upload phases * style: mark successful task completion * fix: preserve storage save error context * fix: serialize single-file progress updates * fix: stabilize batch upload progress reporting Correct batch upload totals, completion state, actual file sizes, and synchronized progress snapshots. Add regression coverage for concurrent updates and phase transitions. * feat: show per-file transfer progress Format single-file and batch download and upload states with Telegram entities, blockquotes, speeds, transferred sizes, progress bars, concise counters, and accurate confirmation handling. Cover upload retries plus final, error, and cancellation messages with regression tests. * chore: remove transfer progress tests * test: restore critical transfer progress coverage * test: cover interleaved batch transfers * fix: declare progress styles in locale templates
196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
package telegram
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gotd/td/telegram/uploader"
|
|
)
|
|
|
|
func TestInitialSegmentDuration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
duration float64
|
|
fileSize int64
|
|
targetSize int64
|
|
want float64
|
|
}{
|
|
{name: "two balanced parts", duration: 120, fileSize: 2200, targetSize: 1900, want: 60},
|
|
{name: "three balanced parts", duration: 120, fileSize: 3900, targetSize: 1900, want: 40},
|
|
{name: "minimum one second", duration: 1.5, fileSize: 3900, targetSize: 1900, want: 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := initialSegmentDuration(tt.duration, tt.fileSize, tt.targetSize)
|
|
if got != tt.want {
|
|
t.Fatalf("initialSegmentDuration() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVideoPartExtension(t *testing.T) {
|
|
tests := map[string]string{
|
|
"clip.MOV": ".mov",
|
|
"clip.mkv": ".mkv",
|
|
"clip.exe": ".mp4",
|
|
"clip": ".mp4",
|
|
}
|
|
for input, want := range tests {
|
|
if got := videoPartExtension(input); got != want {
|
|
t.Fatalf("videoPartExtension(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPartStoragePath(t *testing.T) {
|
|
tests := map[string]string{
|
|
"clip.mov": "clip.part001.mov",
|
|
"123456/clip.mov": "123456/clip.part001.mov",
|
|
"/123456/clip.mov": "/123456/clip.part001.mov",
|
|
"folder/sub/clip.mov": "folder/sub/clip.part001.mov",
|
|
}
|
|
for input, want := range tests {
|
|
if got := partStoragePath(input, "clip.part001.mov"); got != want {
|
|
t.Fatalf("partStoragePath(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVideoPartCaption(t *testing.T) {
|
|
if got := videoPartCaption(nil, 0); got != nil {
|
|
t.Fatalf("caption without source = %q, want nil", *got)
|
|
}
|
|
|
|
source := "original caption"
|
|
first := videoPartCaption(&source, 0)
|
|
if first == nil || *first != source {
|
|
t.Fatalf("first part caption = %v, want %q", first, source)
|
|
}
|
|
second := videoPartCaption(&source, 1)
|
|
if second == nil || *second != "" {
|
|
t.Fatalf("second part caption = %v, want an explicit empty caption", second)
|
|
}
|
|
}
|
|
|
|
func TestResetLosslessVideoUploadProgressUsesCombinedPartSize(t *testing.T) {
|
|
var uploaded, total int64
|
|
progress := newUploadProgress(999, func(current, size int64) {
|
|
uploaded = current
|
|
total = size
|
|
})
|
|
resetLosslessVideoUploadProgress(progress, []losslessVideoPart{
|
|
{Size: 100},
|
|
{Size: 250},
|
|
})
|
|
|
|
if err := progress.Chunk(t.Context(), uploader.ProgressState{ID: 1, Uploaded: 50, Total: 100}); err != nil {
|
|
t.Fatalf("Chunk() failed: %v", err)
|
|
}
|
|
if uploaded != 50 || total != 350 {
|
|
t.Fatalf("lossless video progress = %d/%d, want 50/350", uploaded, total)
|
|
}
|
|
}
|
|
|
|
func TestSplitLosslessVideoRetriesOversizedPart(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
inputPath := filepath.Join(tempDir, "source.mov")
|
|
if err := os.WriteFile(inputPath, []byte("source"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outputDir := filepath.Join(tempDir, "parts")
|
|
if err := os.Mkdir(outputDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
originalRunner := runMediaTool
|
|
t.Cleanup(func() { runMediaTool = originalRunner })
|
|
ffmpegCalls := 0
|
|
runMediaTool = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
switch name {
|
|
case "ffprobe":
|
|
return []byte("120.0\n"), nil
|
|
case "ffmpeg":
|
|
ffmpegCalls++
|
|
pattern := args[len(args)-1]
|
|
sizes := []int{2001, 199}
|
|
if ffmpegCalls > 1 {
|
|
sizes = []int{1100, 1100}
|
|
}
|
|
for index, size := range sizes {
|
|
partPath := strings.Replace(pattern, "%03d", fmt.Sprintf("%03d", index+1), 1)
|
|
if err := os.WriteFile(partPath, make([]byte, size), 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, nil
|
|
default:
|
|
return nil, fmt.Errorf("unexpected media tool %q", name)
|
|
}
|
|
}
|
|
|
|
parts, err := splitLosslessVideo(t.Context(), inputPath, outputDir, "example.mov", 2200, 2000)
|
|
if err != nil {
|
|
t.Fatalf("splitLosslessVideo() failed: %v", err)
|
|
}
|
|
if ffmpegCalls != 2 {
|
|
t.Fatalf("ffmpeg calls = %d, want 2", ffmpegCalls)
|
|
}
|
|
if len(parts) != 2 {
|
|
t.Fatalf("parts = %d, want 2", len(parts))
|
|
}
|
|
if parts[0].Name != "example.part001.mov" || parts[1].Name != "example.part002.mov" {
|
|
t.Fatalf("unexpected part names: %#v", parts)
|
|
}
|
|
for _, part := range parts {
|
|
if part.Size > 2000 {
|
|
t.Fatalf("part %s exceeds limit: %d", part.Name, part.Size)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSplitLosslessVideoRejectsMultipleAlbums(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
inputPath := filepath.Join(tempDir, "source.mov")
|
|
if err := os.WriteFile(inputPath, []byte("source"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outputDir := filepath.Join(tempDir, "parts")
|
|
if err := os.Mkdir(outputDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
originalRunner := runMediaTool
|
|
t.Cleanup(func() { runMediaTool = originalRunner })
|
|
runMediaTool = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
switch name {
|
|
case "ffprobe":
|
|
return []byte("120.0\n"), nil
|
|
case "ffmpeg":
|
|
pattern := args[len(args)-1]
|
|
for index := 0; index < maxLosslessVideoParts+1; index++ {
|
|
partPath := strings.Replace(pattern, "%03d", fmt.Sprintf("%03d", index+1), 1)
|
|
if err := os.WriteFile(partPath, make([]byte, 100), 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, nil
|
|
default:
|
|
return nil, fmt.Errorf("unexpected media tool %q", name)
|
|
}
|
|
}
|
|
|
|
_, err := splitLosslessVideo(t.Context(), inputPath, outputDir, "example.mov", 2200, 2000)
|
|
if err == nil {
|
|
t.Fatal("splitLosslessVideo() succeeded with more than one album of parts")
|
|
}
|
|
if !strings.Contains(err.Error(), "single-album limit") {
|
|
t.Fatalf("splitLosslessVideo() error = %q, want single-album limit", err)
|
|
}
|
|
}
|