check / check (push) Canceled after 0s
magic.DetectFormat returns an imagerequest.Format. ValidateMagicBytes returns the format it checked against the upstream Content-Type. The cache stores that format in the source's row, so a variant made from a cached source uses it too. The proxy hands it to Process, which still refuses input libvips would load with another loader, such as an SVG passing as AVIF. Format.MIMEType declares each MIME string once, and the fetcher, the cache's variant read and the Accept matching use it. magic.MIMEType, magic.ImageFormat, MIMEToImageFormat, magic's SVG detection, detectFormat, formatFromString, FormatToMIME and Result.InputFormat are gone. Model: opus-5-5
134 lines
3.5 KiB
Go
134 lines
3.5 KiB
Go
package imageproxy
|
|
|
|
import (
|
|
"image/color"
|
|
"image/jpeg"
|
|
"io"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/imageprocessor"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// widthOnlyRequest asks for the test photo at width, its height scaled to
|
|
// keep the photo's aspect ratio.
|
|
func widthOnlyRequest(fixtures *TestFixtures, width int) *imagerequest.Request {
|
|
return &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: fixtures.GoodHost,
|
|
Path: testPathPhoto,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: width},
|
|
Format: imagerequest.FormatJPEG,
|
|
Quality: 85,
|
|
FitMode: imagerequest.FitCover,
|
|
},
|
|
}
|
|
}
|
|
|
|
// holdProcessingSlot takes one of proc's processing slots and returns the
|
|
// func that gives it back. Process takes its slot before it reads its input,
|
|
// so once it has read a byte from the pipe it holds the slot, until the pipe
|
|
// is closed.
|
|
func holdProcessingSlot(
|
|
t *testing.T, proc *imageprocessor.ImageProcessor,
|
|
) func() {
|
|
t.Helper()
|
|
|
|
input, feed := io.Pipe()
|
|
|
|
go func() {
|
|
_, _ = proc.Process(t.Context(), input, imagerequest.FormatJPEG,
|
|
&imagerequest.Transform{})
|
|
}()
|
|
|
|
_, err := feed.Write([]byte{0})
|
|
if err != nil {
|
|
t.Fatalf("Process call to hold the slot did not start: %v", err)
|
|
}
|
|
|
|
release := func() { _ = feed.Close() }
|
|
t.Cleanup(release)
|
|
|
|
return release
|
|
}
|
|
|
|
// TestProxy_Get_WaitsForSlotBeforeReadingCachedSource checks that a
|
|
// request whose source is cached holds none of it while it waits for a
|
|
// processing slot: it reads the cached file only once it has a slot. With
|
|
// the only slot held, a request for a new width of the cached 100x100 photo
|
|
// waits; the cached file is then rewritten as a 100x50 image before the slot
|
|
// is freed, so the request must answer with that image scaled to 40x20.
|
|
func TestProxy_Get_WaitsForSlotBeforeReadingCachedSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, fixtures := setupTestProxy(t)
|
|
svc.processor = imageprocessor.New(
|
|
imageprocessor.Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1},
|
|
)
|
|
|
|
// A first request caches the photo as a source.
|
|
resp, err := svc.Get(t.Context(), widthOnlyRequest(fixtures, 50))
|
|
if err != nil {
|
|
t.Fatalf("first Get() error = %v", err)
|
|
}
|
|
|
|
_ = resp.Content.Close()
|
|
|
|
cachedSource := widthOnlyRequest(fixtures, 50).Source
|
|
|
|
reader, _, _, err := svc.cache.Source(t.Context(), cachedSource)
|
|
if err != nil {
|
|
t.Fatalf("Source() error = %v; want the cached source", err)
|
|
}
|
|
|
|
_ = reader.Close()
|
|
|
|
release := holdProcessingSlot(t, svc.processor)
|
|
|
|
var (
|
|
waited *ImageResponse
|
|
waitedErr error
|
|
)
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(done)
|
|
|
|
waited, waitedErr = svc.Get(t.Context(), widthOnlyRequest(fixtures, 40))
|
|
}()
|
|
|
|
// Give the request time to reach the slot: had it read the cached source
|
|
// before waiting, it would have read it by now.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
err = os.WriteFile(sourceFilePath(svc, cachedSource.Key()),
|
|
generateTestJPEG(t, 100, 50, color.RGBA{0, 0, 255, 255}), 0o600)
|
|
if err != nil {
|
|
t.Fatalf("failed to rewrite the cached source: %v", err)
|
|
}
|
|
|
|
release()
|
|
<-done
|
|
|
|
if waitedErr != nil {
|
|
t.Fatalf("Get() error = %v", waitedErr)
|
|
}
|
|
|
|
defer func() { _ = waited.Content.Close() }()
|
|
|
|
output, err := jpeg.DecodeConfig(waited.Content)
|
|
if err != nil {
|
|
t.Fatalf("failed to decode the response: %v", err)
|
|
}
|
|
|
|
if output.Width != 40 || output.Height != 20 {
|
|
t.Errorf("response is %dx%d, want 40x20: the request read the cached "+
|
|
"source before it had a processing slot", output.Width, output.Height)
|
|
}
|
|
}
|