check / check (push) Canceled after 0s
The new package internal/providers builds the cache, the fetcher, the processor, the image service, the session manager and the encrypted URL generator from the config. pixad's fx graph provides them with providers.Constructors(), and handlers.New receives them built. The server tests build the same graph, with only the fetcher's dial function replaced. database.New opens the database, so these parts can read it while fx builds them; the cache's stop hook still runs before the database's. Each setting's default is declared in internal/config, and internal/providers works out the default cache_max_bytes. The cache's MaxBytes is the one value for the disk cache limit, and 0 turns the disk cache off. The fetcher and the processor take explicit values. Model: opus-5-5
107 lines
2.8 KiB
Go
107 lines
2.8 KiB
Go
package imageprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// readerAfterDeadline is a Process input whose first Read waits until done
|
|
// is closed, so the deadline passes after Process has taken its slot and
|
|
// before it encodes.
|
|
type readerAfterDeadline struct {
|
|
data *bytes.Reader
|
|
done <-chan struct{}
|
|
waited bool
|
|
}
|
|
|
|
func (r *readerAfterDeadline) Read(p []byte) (int, error) {
|
|
if !r.waited {
|
|
r.waited = true
|
|
|
|
<-r.done
|
|
}
|
|
|
|
return r.data.Read(p)
|
|
}
|
|
|
|
// inputNotToBeRead is a Process input that fails the test when it is read.
|
|
type inputNotToBeRead struct {
|
|
t *testing.T
|
|
}
|
|
|
|
func (r inputNotToBeRead) Read([]byte) (int, error) {
|
|
r.t.Error("Process read its input although its deadline had passed")
|
|
|
|
return 0, io.EOF
|
|
}
|
|
|
|
// TestProcessStopsWhenDeadlinePassed checks that Process returns
|
|
// context.DeadlineExceeded, and holds no slot afterwards, when its context's
|
|
// deadline has passed before it starts, then without reading its input, or
|
|
// while it reads its input. govips gives pixa no way to stop libvips once an
|
|
// encode has begun, so Process must not begin one for a request that has
|
|
// ended.
|
|
func TestProcessStopsWhenDeadlinePassed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := createTestJPEG(t, 50, 50)
|
|
|
|
t.Run("before Process starts", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithDeadline(t.Context(),
|
|
time.Now().Add(-time.Second))
|
|
defer cancel()
|
|
|
|
// With a slot free, the select in acquireSlot takes the slot or sees
|
|
// the ended context at random, so Process is called many times
|
|
for range 100 {
|
|
checkProcessStopsAtDeadline(ctx, t, inputNotToBeRead{t: t})
|
|
}
|
|
})
|
|
|
|
t.Run("while Process reads its input", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
checkProcessStopsAtDeadline(ctx, t, &readerAfterDeadline{
|
|
data: bytes.NewReader(input), done: ctx.Done(),
|
|
})
|
|
})
|
|
}
|
|
|
|
// checkProcessStopsAtDeadline runs Process on input with ctx, whose deadline
|
|
// has passed by the time Process could encode, and fails the test unless
|
|
// Process returns context.DeadlineExceeded and gives its slot back.
|
|
func checkProcessStopsAtDeadline(
|
|
ctx context.Context, t *testing.T, input io.Reader,
|
|
) {
|
|
t.Helper()
|
|
|
|
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
|
|
|
result, err := proc.Process(ctx, input, smallJPEGTransform())
|
|
if result != nil {
|
|
t.Logf("Process() made a %dx%d image after its deadline passed",
|
|
result.Width, result.Height)
|
|
|
|
_ = result.Content.Close()
|
|
}
|
|
|
|
t.Logf("Process() error = %v", err)
|
|
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Errorf("Process() error = %v, want context.DeadlineExceeded", err)
|
|
}
|
|
|
|
if held := len(proc.processingSemaphore); held != 0 {
|
|
t.Errorf("%d slots still held after Process() returned", held)
|
|
}
|
|
}
|