test(gemini): verify cancel goroutine exits when Download returns

This commit is contained in:
2026-06-30 20:21:00 +02:00
parent 3752b3d3eb
commit 185e6ec20a
2 changed files with 127 additions and 0 deletions
+79
View File
@@ -16,6 +16,7 @@ import (
"net"
"net/url"
"os"
"runtime"
"strconv"
"strings"
"testing"
@@ -450,3 +451,81 @@ func TestSaveGeminiResumeNoop(t *testing.T) {
// Should not panic.
})
}
// TestGeminiCancelGoroutineExits verifies that the ctx-cancellation
// goroutine spawned by Gemini downloadWithRedirects does not outlive
// Download itself. The goroutine used to wait forever on <-ctx.Done()
// when the parent context was never cancelled; the done-channel bound
// must let every iteration return to the runtime goroutine pool.
func TestGeminiCancelGoroutineExits(t *testing.T) {
cfg := testTLSConfig()
listener, err := tls.Listen("tcp", "127.0.0.1:0", cfg)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
port := listener.Addr().(*net.TCPAddr).Port
go func() {
for {
conn, lerr := listener.Accept()
if lerr != nil {
return
}
go func(c net.Conn) {
defer c.Close()
r := bufio.NewReader(c)
r.ReadString('\n')
// Brief response that Gemini Download can chew
// through quickly, spawning the cancellation
// goroutine and returning. The goroutine must
// exit via the done channel on return.
c.Write([]byte("20 application/octet-stream\r\n"))
c.Write([]byte("hi"))
}(conn)
}
}()
// Warm up to settle the runtime goroutine pool (timer, GC, etc.).
proto := NewProtocol()
proto.TLSInsecure = true
u, _ := url.Parse("gemini://127.0.0.1:" + strconv.Itoa(port) + "/warmup")
if _, dlErr := proto.Download(context.Background(), &core.DownloadRequest{
URL: u,
Writer: &stringWriter{&strings.Builder{}},
}); dlErr != nil {
t.Fatalf("warmup: %v", dlErr)
}
runtime.GC()
time.Sleep(50 * time.Millisecond)
runtime.GC()
baseline := runtime.NumGoroutine()
const iterations = 50
for i := 0; i < iterations; i++ {
u, _ := url.Parse("gemini://127.0.0.1:" + strconv.Itoa(port) + "/x")
if _, dlErr := proto.Download(context.Background(), &core.DownloadRequest{
URL: u,
Writer: &stringWriter{&strings.Builder{}},
}); dlErr != nil {
t.Fatalf("download %d: %v", i, dlErr)
}
}
// After every successful Download returns, its cancellation
// goroutine must have exited. Allow a brief settle window for
// the runtime to reclaim them.
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
runtime.GC()
time.Sleep(50 * time.Millisecond)
// One tolerance for any incidental stdlib goroutine that
// the Go runtime occasionally spins up between calls.
if runtime.NumGoroutine() <= baseline+1 {
return
}
}
t.Fatalf("goroutine leak: baseline=%d, after %d downloads = %d",
baseline, iterations, runtime.NumGoroutine())
}