+42
-13
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -32,9 +33,20 @@ func TestShutdownContext_IsCancelledBySIGTERMAndSIGINT(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// serveHTTP must not return while a request is in flight or the worker is still recording
|
// serveHTTP must not return while a request is in flight or the worker is still recording
|
||||||
// its job: ListenAndServe returns ErrServerClosed the moment Shutdown starts, so returning
|
// its job: Serve returns http.ErrServerClosed as soon as Shutdown STARTS, and returning
|
||||||
// from it let the process exit before either finished (#50).
|
// from it let the process exit before either finished (#50). Each half is tested with the
|
||||||
func TestServeHTTP_ReturnsOnlyAfterTheInFlightRequestAndTheWorkerHaveFinished(t *testing.T) {
|
// other already satisfied, so neither can hold the call open on the other's behalf.
|
||||||
|
|
||||||
|
func pendingFor(t *testing.T, returned <-chan error, why string) {
|
||||||
|
t.Helper()
|
||||||
|
select {
|
||||||
|
case err := <-returned:
|
||||||
|
t.Fatalf("serveHTTP returned (%v) %s", err, why)
|
||||||
|
case <-time.After(150 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServeHTTP_DrainsAnInFlightRequestBeforeReturning(t *testing.T) {
|
||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
entered := make(chan struct{})
|
entered := make(chan struct{})
|
||||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
@@ -44,8 +56,12 @@ func TestServeHTTP_ReturnsOnlyAfterTheInFlightRequestAndTheWorkerHaveFinished(t
|
|||||||
}))
|
}))
|
||||||
srv.Start()
|
srv.Start()
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
var once sync.Once
|
||||||
|
releaseHandler := func() { once.Do(func() { close(release) }) }
|
||||||
|
defer releaseHandler() // runs before srv.Close: a failing test must not hang on the blocked handler
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
workerDone := make(chan struct{})
|
workerDone := make(chan struct{})
|
||||||
|
close(workerDone) // the worker is already done: only the request can hold serveHTTP open
|
||||||
returned := make(chan error, 1)
|
returned := make(chan error, 1)
|
||||||
go func() { returned <- serveHTTP(ctx, srv.Config, srv.Listener, nil, workerDone, 5*time.Second) }()
|
go func() { returned <- serveHTTP(ctx, srv.Config, srv.Listener, nil, workerDone, 5*time.Second) }()
|
||||||
|
|
||||||
@@ -53,17 +69,30 @@ func TestServeHTTP_ReturnsOnlyAfterTheInFlightRequestAndTheWorkerHaveFinished(t
|
|||||||
<-entered
|
<-entered
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
assertPending := func(why string) {
|
pendingFor(t, returned, "while a request was still in flight")
|
||||||
t.Helper()
|
releaseHandler()
|
||||||
select {
|
select {
|
||||||
case err := <-returned:
|
case err := <-returned:
|
||||||
t.Fatalf("serveHTTP returned (%v) %s", err, why)
|
if err != nil {
|
||||||
case <-time.After(150 * time.Millisecond):
|
t.Fatalf("serveHTTP: %v", err)
|
||||||
}
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("serveHTTP did not return once the request finished")
|
||||||
}
|
}
|
||||||
assertPending("while a request was still in flight")
|
}
|
||||||
close(release)
|
|
||||||
assertPending("before the worker finished")
|
func TestServeHTTP_WaitsForTheWorkerBeforeReturning(t *testing.T) {
|
||||||
|
srv := httptest.NewUnstartedServer(http.NotFoundHandler())
|
||||||
|
srv.Start()
|
||||||
|
defer srv.Close()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
workerDone := make(chan struct{})
|
||||||
|
returned := make(chan error, 1)
|
||||||
|
go func() { returned <- serveHTTP(ctx, srv.Config, srv.Listener, nil, workerDone, 5*time.Second) }() // no request in flight
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
pendingFor(t, returned, "before the worker finished")
|
||||||
close(workerDone)
|
close(workerDone)
|
||||||
select {
|
select {
|
||||||
case err := <-returned:
|
case err := <-returned:
|
||||||
@@ -71,7 +100,7 @@ func TestServeHTTP_ReturnsOnlyAfterTheInFlightRequestAndTheWorkerHaveFinished(t
|
|||||||
t.Fatalf("serveHTTP: %v", err)
|
t.Fatalf("serveHTTP: %v", err)
|
||||||
}
|
}
|
||||||
case <-time.After(2 * time.Second):
|
case <-time.After(2 * time.Second):
|
||||||
t.Fatal("serveHTTP did not return once the request and the worker had finished")
|
t.Fatal("serveHTTP did not return once the worker finished")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package uploads_test
|
||||||
|
|
||||||
|
// The limits production runs on are the DEFAULTS: nothing outside tests sets
|
||||||
|
// MaxReceives or MaxReceivesPerUser. Pinned here with the literal numbers, so changing a
|
||||||
|
// default is a visible decision (#47 review). With 4 receives, 20 queued and 1 running at
|
||||||
|
// the 200 MiB cap, the spool needs ~5 GiB of the 6 Gi emptyDir (infra); raising any of
|
||||||
|
// them or TAPIR_UPLOAD_MAX_BYTES must revisit that arithmetic.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.d-ma.be/mathias/tapir/internal/uploads"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewIntake_DefaultsAllowTwoReceivesPerUserAndFourOverall(t *testing.T) {
|
||||||
|
r := newRig(t)
|
||||||
|
in := uploads.NewIntake(r.st, r.pr, r.sub, uploads.Limits{MaxBytes: 1024, MaxDuration: time.Hour}, r.tmp)
|
||||||
|
startSlowReceive(t, r, in, "a", 1)
|
||||||
|
startSlowReceive(t, r, in, "a", 2)
|
||||||
|
|
||||||
|
_, err := in.Accept(context.Background(), "a", "t", strings.NewReader("x"))
|
||||||
|
requireBusy(t, err) // a third body from one user
|
||||||
|
|
||||||
|
startSlowReceive(t, r, in, "b", 3)
|
||||||
|
startSlowReceive(t, r, in, "b", 4)
|
||||||
|
_, err = in.Accept(context.Background(), "c", "t", strings.NewReader("x"))
|
||||||
|
requireBusy(t, err) // a fifth body overall
|
||||||
|
}
|
||||||
@@ -53,12 +53,17 @@ func hasSkipNotice(out string) bool {
|
|||||||
|
|
||||||
// runLint runs `task lint` in dir with the given PATH and returns its combined output and exit code.
|
// runLint runs `task lint` in dir with the given PATH and returns its combined output and exit code.
|
||||||
func runLint(t *testing.T, dir, path string) (string, int) {
|
func runLint(t *testing.T, dir, path string) (string, int) {
|
||||||
|
t.Helper()
|
||||||
|
return runTask(t, dir, path, "lint")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTask(t *testing.T, dir, path string, args ...string) (string, int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
taskBin, err := exec.LookPath("task")
|
taskBin, err := exec.LookPath("task")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("task not installed")
|
t.Skip("task not installed")
|
||||||
}
|
}
|
||||||
cmd := exec.Command(taskBin, "lint")
|
cmd := exec.Command(taskBin, args...)
|
||||||
cmd.Dir = dir
|
cmd.Dir = dir
|
||||||
cmd.Env = append(os.Environ(), "PATH="+path)
|
cmd.Env = append(os.Environ(), "PATH="+path)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
@@ -87,6 +92,20 @@ func TestTaskLint_ExitsNonZeroWhenGolangciLintReportsAFinding(t *testing.T) {
|
|||||||
if hasSkipNotice(out) {
|
if hasSkipNotice(out) {
|
||||||
t.Fatalf("task lint blamed a missing binary for a lint failure; output:\n%s", out)
|
t.Fatalf("task lint blamed a missing binary for a lint failure; output:\n%s", out)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(out, "errcheck") {
|
||||||
|
t.Fatalf("a non-zero exit without the finding means the linter crashed, not that it found something; output:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lint step only gates anything if `task check` runs it.
|
||||||
|
func TestTaskCheck_RunsTheLintStep(t *testing.T) {
|
||||||
|
out, code := runTask(t, scratchModule(t), os.Getenv("PATH"), "--dry", "check")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("task --dry check exited %d; output:\n%s", code, out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "task: [lint]") {
|
||||||
|
t.Fatalf("task check no longer runs the lint step; output:\n%s", out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTaskLint_SkipsWithANoticeWhenGolangciLintIsAbsent(t *testing.T) {
|
func TestTaskLint_SkipsWithANoticeWhenGolangciLintIsAbsent(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user