mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
199 lines
5.4 KiB
Go
199 lines
5.4 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestHTTPSpoolRangeCrossBlocksAndRefetch(t *testing.T) {
|
|
data := make([]byte, 4*SpoolBlockSize+57)
|
|
for i := range data {
|
|
data[i] = byte(i*31 + i/257)
|
|
}
|
|
var large atomic.Bool
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var start, end int64
|
|
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
|
if end-start+1 > SpoolBlockSize {
|
|
large.Store(true)
|
|
}
|
|
w.Header().Set("ETag", `"stable"`)
|
|
http.ServeContent(w, r, "media", time.Time{}, bytes.NewReader(data))
|
|
}))
|
|
defer srv.Close()
|
|
input, err := OpenSpoolingHTTP(context.Background(), srv.Client(), HTTPSource{URL: srv.URL}, t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer input.Close()
|
|
h := input.(*HTTPSpool)
|
|
select {
|
|
case <-h.done:
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("prefetch did not finish")
|
|
}
|
|
if !large.Load() {
|
|
t.Fatal("did not fetch multiple blocks in one response")
|
|
}
|
|
for _, offset := range []int64{3*SpoolBlockSize - 123, 0, 2*SpoolBlockSize + 41, SpoolBlockSize - 5} {
|
|
if _, err := h.Seek(offset, io.SeekStart); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := make([]byte, min(int64(2*SpoolBlockSize), int64(len(data))-offset))
|
|
if _, err := io.ReadFull(h, got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(got, data[offset:offset+int64(len(got))]) {
|
|
t.Fatal("incorrect random/cross-block read", offset)
|
|
}
|
|
}
|
|
unique, received := h.Progress()
|
|
if unique != int64(len(data)) || received <= unique {
|
|
t.Fatal("refetch progress must not double-count stored bytes", unique, received)
|
|
}
|
|
}
|
|
|
|
func TestHTTPSpoolSequentialAndCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
entered := make(chan struct{})
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("hello"))
|
|
w.(http.Flusher).Flush()
|
|
close(entered)
|
|
<-r.Context().Done()
|
|
}))
|
|
defer srv.Close()
|
|
in, err := OpenSpoolingHTTP(ctx, srv.Client(), HTTPSource{URL: srv.URL}, t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
<-entered
|
|
if in.Seekable() {
|
|
t.Fatal("200 response advertised seek")
|
|
}
|
|
got := make([]byte, 5)
|
|
if _, err := io.ReadFull(in, got); err != nil || string(got) != "hello" {
|
|
t.Fatal(string(got), err)
|
|
}
|
|
cancel()
|
|
if err := in.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPSpoolRejectsBadRanges(t *testing.T) {
|
|
for _, mode := range []string{"etag", "length", "offset", "status", "truncated", "encoding"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
var calls atomic.Int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
first := calls.Add(1) == 1
|
|
start, end := int64(0), int64(httpBlockSize-1)
|
|
if !first {
|
|
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
|
}
|
|
tag := `"stable"`
|
|
if !first && mode == "etag" {
|
|
tag = `"changed"`
|
|
}
|
|
w.Header().Set("ETag", tag)
|
|
if !first && mode == "status" {
|
|
w.WriteHeader(200)
|
|
return
|
|
}
|
|
if !first && mode == "offset" {
|
|
start++
|
|
}
|
|
if !first && mode == "encoding" {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
}
|
|
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, 2*SpoolBlockSize))
|
|
length := end - start + 1
|
|
if !first && mode == "length" {
|
|
length++
|
|
}
|
|
w.Header().Set("Content-Length", fmt.Sprint(length))
|
|
w.WriteHeader(206)
|
|
if !first && mode == "truncated" {
|
|
length /= 2
|
|
}
|
|
chunk := make([]byte, 64*1024)
|
|
for length > 0 {
|
|
n := min(int64(len(chunk)), length)
|
|
if _, err := w.Write(chunk[:n]); err != nil {
|
|
return
|
|
}
|
|
length -= n
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
in, err := OpenSpoolingHTTP(context.Background(), srv.Client(), HTTPSource{URL: srv.URL}, t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer in.Close()
|
|
h := in.(*HTTPSpool)
|
|
select {
|
|
case err := <-h.Failures():
|
|
if err == nil {
|
|
t.Fatal("nil failure")
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("bad range did not fail")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHTTPSpoolReadsPartialRangeBeforeResponseFinishes(t *testing.T) {
|
|
ready, finish := make(chan struct{}), make(chan struct{})
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var start, end int64
|
|
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
|
end = min(end, 4*1024*1024-1)
|
|
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, 4*1024*1024))
|
|
w.Header().Set("Content-Length", fmt.Sprint(end-start+1))
|
|
w.WriteHeader(206)
|
|
if start == 0 {
|
|
w.Write(make([]byte, end+1))
|
|
return
|
|
}
|
|
w.Write(bytes.Repeat([]byte{93}, 64*1024))
|
|
w.(http.Flusher).Flush()
|
|
close(ready)
|
|
select {
|
|
case <-finish:
|
|
w.Write(make([]byte, end-start+1-64*1024))
|
|
case <-r.Context().Done():
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
input, err := OpenSpoolingHTTP(ctx, srv.Client(), HTTPSource{URL: srv.URL}, t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer input.Close()
|
|
select {
|
|
case <-ready:
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
if _, err := input.Seek(1024*1024, io.SeekStart); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := make([]byte, 1)
|
|
if _, err := io.ReadFull(input, b); err != nil || b[0] != 93 {
|
|
t.Fatal("waited for complete response", b, err)
|
|
}
|
|
close(finish)
|
|
}
|