Files
yetoneandClaude Opus 5.5 30a8ce2915 davsync tests: TestSlowTransferIsNotCutShort's watchdog is a second, not 300 ms, over ~24 MB each way
A line that never stops is all this test sends, so a 300 ms watchdog only
catches the runner itself pausing: red on macOS in run 37693090857 at
7.99s, near the end of the GET, where a full run takes 7.23s and 8 runs
under 3x CPU oversubscription here all passed. The server's 40 ms gap is
still 25x inside the watchdog, the PUT's tail is still twice it, and with
allSent's extension taken out the test still fails ("took nothing for 1s
(PUT)").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:21:51 +08:00

254 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package davsync
import (
"bytes"
"context"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"strconv"
"sync"
"testing"
"time"
)
// shortStall makes the watchdogs short for a test.
func shortStall(t *testing.T, stall, meta time.Duration) {
s, m := stallAfter, metaLimit
stallAfter, metaLimit = stall, meta
t.Cleanup(func() { stallAfter, metaLimit = s, m })
}
// slowDAV is a WebDAV server on a slow line: the backup goes both ways in
// chunks of chunk bytes every gap, or stops after stopAt bytes for good. A
// PUT is answered tail after its body is all in: the time the last of it,
// handed over by magpie, would take to cross the line.
type slowDAV struct {
chunk int
gap time.Duration
tail time.Duration
stopAt int // 0: never
quiet chan struct{} // closed when the test is done with a stopped server
mu sync.Mutex
stored []byte
}
// stop holds the request without a byte either way until the client gives
// up, or the test ends (a server doesn't see a client gone before it has
// read the body).
func (s *slowDAV) stop(r *http.Request) {
select {
case <-r.Context().Done():
case <-s.quiet:
}
}
func (s *slowDAV) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut:
var got bytes.Buffer
got.Grow(int(r.ContentLength))
buf := make([]byte, s.chunk)
for {
if s.stopAt > 0 && got.Len() >= s.stopAt {
s.stop(r) // the line went quiet
return
}
n, err := io.ReadFull(r.Body, buf)
got.Write(buf[:n])
if err != nil {
break
}
time.Sleep(s.gap)
}
time.Sleep(s.tail)
s.mu.Lock()
s.stored = got.Bytes()
s.mu.Unlock()
w.Header().Set("ETag", `"v1"`)
w.WriteHeader(http.StatusCreated)
case http.MethodHead:
s.mu.Lock()
w.Header().Set("Content-Length", strconv.Itoa(len(s.stored)))
s.mu.Unlock()
w.WriteHeader(http.StatusOK)
case http.MethodGet:
s.mu.Lock()
data := s.stored
s.mu.Unlock()
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Header().Set("ETag", `"v1"`)
w.WriteHeader(http.StatusOK)
for i := 0; i < len(data); i += s.chunk {
if s.stopAt > 0 && i >= s.stopAt {
s.stop(r)
return
}
w.Write(data[i:min(i+s.chunk, len(data))])
w.(http.Flusher).Flush()
time.Sleep(s.gap)
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
// narrowLine serves h, and gives the client that reaches it, with the
// line's buffers held small. The system hands a writer room in bursts: Linux wakes it once a third
// of its send buffer has drained and opens the receiver's window by as much,
// and with the MBs it tunes both up to on a fast loopback, a body moving at
// 6.4 MB/s went 150–290 ms between two of magpie's writes, which a test's
// watchdog of 300 ms didn't always outlast though the line never stopped (at
// the real 60 s that is a line under ~25 KB/s). Small buffers keep the
// bursts to a chunk or two; what sits in buffers after the body is handed
// over is the server's tail.
func narrowLine(t *testing.T, h http.Handler) (*httptest.Server, *http.Client) {
const buffer = 128 << 10
srv := httptest.NewUnstartedServer(h)
srv.Listener = narrowListener{srv.Listener, buffer}
srv.Start()
t.Cleanup(srv.Close)
tr := http.DefaultTransport.(*http.Transport).Clone()
dial := tr.DialContext
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
c, err := dial(ctx, network, addr)
if err == nil {
err = narrow(c, buffer)
}
return c, err
}
t.Cleanup(tr.CloseIdleConnections)
return srv, &http.Client{Transport: stallTransport{tr}}
}
type narrowListener struct {
net.Listener
buffer int
}
func (l narrowListener) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err == nil {
err = narrow(c, l.buffer)
}
return c, err
}
// narrow keeps both ways of a connection small: the PUT goes up through the
// client's send buffer and the server's receive buffer, the GET comes down
// through the other two, and on macOS a download through the tuned-up pair
// burst as the upload had (run 37693090857: "took nothing for 300ms (GET)").
func narrow(c net.Conn, buffer int) error {
tc := c.(*net.TCPConn)
if err := tc.SetReadBuffer(buffer); err != nil {
return err
}
return tc.SetWriteBuffer(buffer)
}
// A backup that takes far longer than the watchdog to go up and come back
// down, but never stops moving, is written and read in full (#657: 43 MB
// at 305 KB/s took 2.4 minutes, and a sync was given 2).
func TestSlowTransferIsNotCutShort(t *testing.T) {
// a second, not the 300 ms the other stall tests use: a macOS runner
// can stop the whole test for a few hundred ms, and here the line never
// stops, so such a pause is all a shorter watchdog would catch (red
// on macOS in run 37693090857, near the end of the GET)
shortStall(t, time.Second, time.Second)
// the PUT's last MBs cross the line twice the watchdog after magpie has
// handed them all over
s := &slowDAV{chunk: 256 << 10, gap: 40 * time.Millisecond, tail: 2 * stallAfter}
srv, client := narrowLine(t, s)
d, err := newDAV(Config{URL: srv.URL})
if err != nil {
t.Fatal(err)
}
d.client = client
// ~24 MB at 6.4 MB/s: nearly 4 s each way
data := bytes.Repeat([]byte("magpie skill "), (24<<20)/13)
ctx := context.Background()
start := time.Now()
if _, err := d.put(ctx, data, ""); err != nil {
t.Fatalf("slow upload: %v", err)
}
up := time.Since(start)
start = time.Now()
got, _, err := d.get(ctx, version{})
if err != nil {
t.Fatalf("slow download: %v", err)
}
down := time.Since(start)
if !bytes.Equal(got, data) {
t.Fatalf("read back %d bytes of %d", len(got), len(data))
}
if up < 3*stallAfter || down < 3*stallAfter {
t.Fatalf("the transfers (%s up, %s down) weren't slow enough to test anything", up, down)
}
}
// A server that stops in the middle, either way, fails the sync soon after
// it stops, saying so — not after however long a sync may take.
func TestStalledTransferFailsPromptly(t *testing.T) {
shortStall(t, 300*time.Millisecond, 300*time.Millisecond)
data := bytes.Repeat([]byte("x"), 32<<20) // more than the system buffers hold
for _, way := range []string{"up", "down"} {
t.Run(way, func(t *testing.T) {
s := &slowDAV{chunk: 64 << 10, gap: 10 * time.Millisecond, quiet: make(chan struct{})}
if way == "up" {
s.stopAt = 1 << 20
} else {
s.stored = data
s.stopAt = 1 << 20
}
srv := httptest.NewServer(s)
defer srv.Close()
defer close(s.quiet)
d, err := newDAV(Config{URL: srv.URL})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
start := time.Now()
if way == "up" {
_, err = d.put(ctx, data, "")
} else {
_, _, err = d.get(ctx, version{})
}
took := time.Since(start)
var st *errStalled
if !errors.As(err, &st) {
t.Fatalf("%v after %s, want a stall", err, took)
}
if took > 10*time.Second {
t.Fatalf("a stall took %s to tell", took)
}
})
}
}
// A request that moves only a little (PROPFIND, MKCOL…) has metaLimit in
// all, however it trickles.
func TestMetadataRequestHasALimit(t *testing.T) {
shortStall(t, 300*time.Millisecond, 300*time.Millisecond)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(5 * time.Second):
case <-r.Context().Done():
}
}))
defer srv.Close()
d, err := newDAV(Config{URL: srv.URL})
if err != nil {
t.Fatal(err)
}
start := time.Now()
err = d.mkcol(context.Background())
var st *errStalled
if !errors.As(err, &st) || time.Since(start) > 3*time.Second {
t.Fatalf("%v after %s, want the limit", err, time.Since(start))
}
}