239 lines
7.4 KiB
Go
239 lines
7.4 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image/color"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/pixa/internal/imageprocessor"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/magic"
|
|
)
|
|
|
|
// TestPutSource_StoresIdenticalBytesOfTwoURLsTwice stores the same bytes
|
|
// for two source URLs and checks that each URL has a source_content row of
|
|
// its own and a file of its own, named by its source key under
|
|
// cache/sources/<ab>/<cd>/.
|
|
func TestPutSource_StoresIdenticalBytesOfTwoURLsTwice(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, stateDir := newEvictionTestCache(t, 1<<30)
|
|
content := bytes.Repeat([]byte{0x5A}, 400)
|
|
|
|
keys := map[string]imagerequest.SourceKey{
|
|
"/a.jpg": storeEvictionTestSource(t, cache, testHostCDN, "/a.jpg", content),
|
|
"/b.jpg": storeEvictionTestSource(t, cache, testHostCDN, "/b.jpg", content),
|
|
}
|
|
t.Logf("source keys: %v", keys)
|
|
|
|
if keys["/a.jpg"] == keys["/b.jpg"] {
|
|
t.Fatalf("both source URLs got the key %s", keys["/a.jpg"])
|
|
}
|
|
|
|
for sourcePath, key := range keys {
|
|
rows := countRows(t, cache,
|
|
`SELECT COUNT(*) FROM source_content WHERE source_key = ?`, string(key))
|
|
if rows != 1 {
|
|
t.Errorf("%s has %d source_content rows, want 1", sourcePath, rows)
|
|
}
|
|
|
|
name := string(key)
|
|
file := filepath.Join(stateDir, "cache", "sources", name[0:2], name[2:4], name)
|
|
|
|
stored, err := os.ReadFile(file) //nolint:gosec // path in the test's state dir
|
|
if err != nil || !bytes.Equal(stored, content) {
|
|
t.Errorf("file of %s at %s holds %d bytes (error %v), want the %d stored",
|
|
sourcePath, file, len(stored), err, len(content))
|
|
}
|
|
}
|
|
|
|
usage, err := cache.UsageBytes(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("UsageBytes() error = %v", err)
|
|
}
|
|
|
|
if usage != 800 {
|
|
t.Errorf("UsageBytes() = %d, want 800, the 400 bytes once for each URL", usage)
|
|
}
|
|
}
|
|
|
|
// TestEvictSourceBlob_LeavesSourceWithIdenticalBytesServed stores the same
|
|
// bytes for two source URLs and evicts the first. Its row and file must be
|
|
// gone, and the second must still be found and read from the cache.
|
|
func TestEvictSourceBlob_LeavesSourceWithIdenticalBytesServed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
content := generateTestJPEG(t, 20, 20, color.RGBA{0x5B, 0x5B, 0x5B, 255})
|
|
|
|
evicted := storeEvictionTestSource(t, cache, testHostCDN, "/evicted.jpg", content)
|
|
storeEvictionTestSource(t, cache, testHostCDN, "/kept.jpg", content)
|
|
|
|
err := cache.evictSourceBlob(t.Context(), evicted)
|
|
if err != nil {
|
|
t.Fatalf("evictSourceBlob() error = %v", err)
|
|
}
|
|
|
|
_, err = os.Stat(cache.srcContent.keyToPath(string(evicted)))
|
|
if err == nil {
|
|
t.Error("the evicted source's file is still on disk")
|
|
}
|
|
|
|
if n := countRows(t, cache,
|
|
`SELECT COUNT(*) FROM source_content WHERE source_key = ?`, string(evicted),
|
|
); n != 0 {
|
|
t.Errorf("the evicted source still has %d source_content rows", n)
|
|
}
|
|
|
|
reader, _, _, err := cache.Source(t.Context(),
|
|
imagerequest.Source{Host: testHostCDN, Path: "/kept.jpg"})
|
|
t.Logf("Source() of the kept source: error %v", err)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Source() of the kept source error = %v; want it cached", err)
|
|
}
|
|
|
|
defer func() { _ = reader.Close() }()
|
|
|
|
served, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("reading the kept source: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(served, content) {
|
|
t.Errorf("the kept source reads as %d bytes, want the %d stored",
|
|
len(served), len(content))
|
|
}
|
|
}
|
|
|
|
// TestPutSource_FailedRowWriteLeavesNoFile drops source_content, so that
|
|
// PutSource cannot write the source's row, and checks that the store fails
|
|
// and leaves no file under the source's key.
|
|
func TestPutSource_FailedRowWriteLeavesNoFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
_, err := cache.db.ExecContext(t.Context(), `DROP TABLE source_content`)
|
|
if err != nil {
|
|
t.Fatalf("failed to drop source_content: %v", err)
|
|
}
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{Host: testHostCDN, Path: testPathCat},
|
|
}
|
|
key, err := cache.PutSource(t.Context(), req.Source,
|
|
bytes.NewReader([]byte("source bytes")), SourceInfo{})
|
|
t.Logf("PutSource() = %q, error = %v", key, err)
|
|
|
|
if err == nil {
|
|
t.Error("PutSource() error = nil, want the failed row write")
|
|
}
|
|
|
|
_, err = os.Stat(cache.srcContent.keyToPath(string(req.Source.Key())))
|
|
if err == nil {
|
|
t.Error("the source's file is on disk although its row was not written")
|
|
}
|
|
}
|
|
|
|
// TestSource_FormatFromFirstBytes stores an image in each source format
|
|
// under one source URL, in turn. After each store, Source must return that
|
|
// image whole, with its format read from the file.
|
|
func TestSource_FormatFromFirstBytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
processor := imageprocessor.New(imageprocessor.Params{
|
|
MaxInputBytes: 50 << 20,
|
|
MaxConcurrentProcessing: 1,
|
|
})
|
|
jpegImage := generateTestJPEG(t, 40, 30, color.RGBA{200, 40, 40, 255})
|
|
src := imagerequest.Source{Host: testHostCDN, Path: testPathCat}
|
|
|
|
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(jpegImage),
|
|
imagerequest.FormatJPEG, &imagerequest.Transform{Format: format})
|
|
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)
|
|
}
|
|
|
|
_, err = cache.PutSource(t.Context(), src, bytes.NewReader(image), SourceInfo{})
|
|
if err != nil {
|
|
t.Fatalf("PutSource(%s) error = %v", format, err)
|
|
}
|
|
|
|
reader, size, got, err := cache.Source(t.Context(), src)
|
|
if err != nil {
|
|
t.Fatalf("Source() of the %s image error = %v", format, err)
|
|
}
|
|
|
|
served, err := io.ReadAll(reader)
|
|
_ = reader.Close()
|
|
|
|
if err != nil {
|
|
t.Fatalf("reading the %s source: %v", format, err)
|
|
}
|
|
|
|
t.Logf("%s: %d bytes read as %s", format, size, got)
|
|
|
|
if got != format {
|
|
t.Errorf("Source() of the %s image format = %q, want %q",
|
|
format, got, format)
|
|
}
|
|
|
|
if size != int64(len(image)) || !bytes.Equal(served, image) {
|
|
t.Errorf("%s source read as %d bytes, size %d, want the %d bytes stored",
|
|
format, len(served), size, len(image))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSource_RefusesFileOfNoSourceFormat checks that Source refuses a
|
|
// cached source whose file's first bytes are not those of a source format,
|
|
// with an error other than ErrNotFound.
|
|
func TestSource_RefusesFileOfNoSourceFormat(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 file", nil, io.EOF},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
path := fmt.Sprintf("/refused%d.jpg", i)
|
|
storeEvictionTestSource(t, cache, testHostCDN, path, tt.content)
|
|
|
|
reader, _, format, err := cache.Source(t.Context(),
|
|
imagerequest.Source{Host: testHostCDN, Path: path})
|
|
t.Logf("%s: Source() error = %v", tt.name, err)
|
|
|
|
if reader != nil || format != "" || !errors.Is(err, tt.wantErr) {
|
|
t.Errorf("%s: Source() = reader %v, format %q, error %v; "+
|
|
"want no reader and %v", tt.name, reader, format, err, tt.wantErr)
|
|
}
|
|
}
|
|
}
|