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 reads a cached source's format from its file's first bytes, as it reads a variant's. The proxy hands the format to Process. Process refuses a format that is not a source format, and 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
215 lines
6.6 KiB
Go
215 lines
6.6 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image/color"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/imageprocessor"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/magic"
|
|
)
|
|
|
|
// TestVariant_ContentTypeFromFirstBytes stores an image in each output
|
|
// format as a variant and checks that Variant serves it whole with the
|
|
// content type of its format, read from the file.
|
|
func TestVariant_ContentTypeFromFirstBytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
processor := imageprocessor.New(imageprocessor.Params{
|
|
MaxInputBytes: 50 << 20,
|
|
MaxConcurrentProcessing: 1,
|
|
})
|
|
source := generateTestJPEG(t, 40, 30, color.RGBA{200, 40, 40, 255})
|
|
|
|
for _, format := range []imagerequest.Format{
|
|
imagerequest.FormatJPEG, imagerequest.FormatPNG, imagerequest.FormatGIF,
|
|
imagerequest.FormatWebP, imagerequest.FormatAVIF, imagerequest.FormatJXL,
|
|
} {
|
|
result, err := processor.Process(t.Context(), bytes.NewReader(source),
|
|
imagerequest.FormatJPEG, &imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 20, Height: 15},
|
|
Format: format,
|
|
Quality: 85,
|
|
FitMode: imagerequest.FitCover,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("making a %s image: %v", format, err)
|
|
}
|
|
|
|
image, err := io.ReadAll(result.Content)
|
|
if err != nil {
|
|
t.Fatalf("reading the %s image: %v", format, err)
|
|
}
|
|
|
|
key := imagerequest.VariantKey("aabbccdd10" + string(format))
|
|
|
|
err = cache.PutVariant(t.Context(), key, bytes.NewReader(image))
|
|
if err != nil {
|
|
t.Fatalf("PutVariant(%s) error = %v", format, err)
|
|
}
|
|
|
|
reader, size, contentType, err := cache.Variant(key)
|
|
if err != nil {
|
|
t.Fatalf("Variant(%s) error = %v", format, err)
|
|
}
|
|
|
|
served, err := io.ReadAll(reader)
|
|
_ = reader.Close()
|
|
|
|
if err != nil {
|
|
t.Fatalf("reading the %s variant: %v", format, err)
|
|
}
|
|
|
|
t.Logf("%s: %d bytes served as %s", format, size, contentType)
|
|
|
|
if want := format.MIMEType(); contentType != want {
|
|
t.Errorf("%s variant content type = %q, want %q", format, contentType, want)
|
|
}
|
|
|
|
if size != int64(len(image)) || !bytes.Equal(served, image) {
|
|
t.Errorf("%s variant served %d bytes, size %d, want the %d bytes stored",
|
|
format, len(served), size, len(image))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestVariant_RefusesFileOfNoOutputFormat checks that Variant refuses
|
|
// a variant file whose first bytes are not those of an output format, with
|
|
// an error other than ErrNotFound, and that a variant with no file, or any
|
|
// variant of a disabled cache, is ErrNotFound.
|
|
func TestVariant_RefusesFileOfNoOutputFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
jpegImage := generateTestJPEG(t, 10, 10, color.RGBA{0, 0, 255, 255})
|
|
|
|
tests := []struct {
|
|
name string
|
|
content []byte
|
|
wantErr error
|
|
}{
|
|
{"text", []byte("this is text, not an image"), magic.ErrUnknownFormat},
|
|
{"SVG", []byte(`<svg xmlns="http://www.w3.org/2000/svg"></svg>`),
|
|
magic.ErrUnknownFormat},
|
|
{"JPEG cut to 10 bytes", jpegImage[:10], io.EOF},
|
|
{"empty", nil, io.EOF},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
key := imagerequest.VariantKey(fmt.Sprintf("aabbccdd20%02d", i))
|
|
|
|
err := cache.variants.Write(string(key), tt.content)
|
|
if err != nil {
|
|
t.Fatalf("%s: storing the variant file: %v", tt.name, err)
|
|
}
|
|
|
|
reader, _, contentType, err := cache.Variant(key)
|
|
t.Logf("%s: Variant() error = %v", tt.name, err)
|
|
|
|
if reader != nil || contentType != "" || !errors.Is(err, tt.wantErr) {
|
|
t.Errorf("%s: Variant() = reader %v, content type %q, error %v; "+
|
|
"want no reader and %v", tt.name, reader, contentType, err, tt.wantErr)
|
|
}
|
|
}
|
|
|
|
reader, _, _, err := cache.Variant(testVariantKeyFour)
|
|
if reader != nil || !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("Variant() of a variant with no file = reader %v, error %v; "+
|
|
"want no reader and %v", reader, err, ErrNotFound)
|
|
}
|
|
|
|
disabled, _ := newEvictionTestCache(t, 0)
|
|
|
|
reader, _, _, err = disabled.Variant(testVariantKeyOne)
|
|
if reader != nil || !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("Variant() on a disabled cache = reader %v, error %v; "+
|
|
"want no reader and %v", reader, err, ErrNotFound)
|
|
}
|
|
}
|
|
|
|
// TestTouchVariantAndTouchSourceContent_DropAccessTimesPastTheLimit hands one
|
|
// more access time of a variant, and one more of a source, than may wait to
|
|
// be written. touchVariant and touchSourceContent must return each time
|
|
// without waiting, and the last of each must be dropped.
|
|
func TestTouchVariantAndTouchSourceContent_DropAccessTimesPastTheLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(done)
|
|
|
|
for range maxPendingAccessTimes + 1 {
|
|
cache.touchVariant(testVariantKeyOne)
|
|
cache.touchSourceContent("aabbccdd3001")
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(pendingAccessTimesWait):
|
|
t.Fatal("touchVariant or touchSourceContent waited once the access " +
|
|
"times reached the limit")
|
|
}
|
|
|
|
if waiting := len(cache.pendingVariantAccessTimes); waiting != maxPendingAccessTimes {
|
|
t.Errorf("%d access times of variants wait to be written, want %d",
|
|
waiting, maxPendingAccessTimes)
|
|
}
|
|
|
|
if waiting := len(cache.pendingSourceAccessTimes); waiting != maxPendingAccessTimes {
|
|
t.Errorf("%d access times of sources wait to be written, want %d",
|
|
waiting, maxPendingAccessTimes)
|
|
}
|
|
}
|
|
|
|
// TestStopPendingAccessTimeWrites_WritesPendingAccessTimes hands the access
|
|
// time of one of two variants, three times, then stops the goroutine that
|
|
// writes them, which never started. The stop must write that access time and
|
|
// leave the other variant's.
|
|
func TestStopPendingAccessTimeWrites_WritesPendingAccessTimes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
anHourAgo := time.Now().Add(-time.Hour)
|
|
|
|
for _, key := range []imagerequest.VariantKey{testVariantKeyOne, testVariantKeyTwo} {
|
|
storeEvictionTestVariant(t, cache, key, []byte("variant bytes"))
|
|
setVariantLastAccessed(t, cache, key, anHourAgo)
|
|
}
|
|
|
|
for range 3 {
|
|
cache.touchVariant(testVariantKeyOne)
|
|
}
|
|
|
|
err := cache.StopPendingAccessTimeWrites(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("StopPendingAccessTimeWrites() error = %v", err)
|
|
}
|
|
|
|
for key, wantTouched := range map[imagerequest.VariantKey]int{
|
|
testVariantKeyOne: 1, testVariantKeyTwo: 0,
|
|
} {
|
|
touched := countRows(t, cache, `
|
|
SELECT COUNT(*) FROM variant_content
|
|
WHERE cache_key = ? AND last_accessed_at > ?
|
|
`, string(key), anHourAgo.UTC().Format(sqliteTimestampFormat))
|
|
if touched != wantTouched {
|
|
t.Errorf("variant %s: %d rows with a new access time, want %d",
|
|
key, touched, wantTouched)
|
|
}
|
|
}
|
|
|
|
if waiting := len(cache.pendingVariantAccessTimes); waiting != 0 {
|
|
t.Errorf("%d access times still wait to be written, want 0", waiting)
|
|
}
|
|
}
|