check / check (push) Waiting to run
The new package internal/imageproxy holds Proxy, which was imgcache.Service, with its Config, ImageResponse, CacheStatus and errors. imageproxy.New makes and registers the counters of upstream fetches and transforms. internal/imgcache holds the cache only, with its hit and miss counters, and imgcache.CacheConfig is imgcache.Config. The cache reads and writes each kind it keeps with one pair of methods: Variant and PutVariant, Source and PutSource, Failure and PutFailure. Source replaces LookupSource followed by GetSourceContent. imgcache.go and module.go are gone. The proxy's tests moved with it. The cache's tests check that the reconciliation pass removes an empty or cut file and its row. The proxy's tests remove them by hand, then check that Get fetches or converts again. Model: opus-5-5
186 lines
5.0 KiB
Go
186 lines
5.0 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// reconcileAsAtStartup runs the cache's reconciliation pass, as pixa does
|
|
// when it starts, and returns what the pass logged.
|
|
func reconcileAsAtStartup(t *testing.T, cache *Cache) string {
|
|
t.Helper()
|
|
|
|
var logBuf bytes.Buffer
|
|
|
|
cache.log = slog.New(slog.NewJSONHandler(&logBuf, nil))
|
|
|
|
err := cache.reconcileAccounting(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("reconcileAccounting() error = %v", err)
|
|
}
|
|
|
|
t.Logf("reconciliation log: %s", logBuf.String())
|
|
|
|
return logBuf.String()
|
|
}
|
|
|
|
// assertWarningNames checks that logs hold a warning naming the file at
|
|
// path.
|
|
func assertWarningNames(t *testing.T, logs, path string) {
|
|
t.Helper()
|
|
|
|
for line := range strings.Lines(logs) {
|
|
if strings.Contains(line, `"level":"WARN"`) &&
|
|
strings.Contains(line, `"path":"`+path+`"`) {
|
|
return
|
|
}
|
|
}
|
|
|
|
t.Errorf("reconciliation logged no warning naming %s", path)
|
|
}
|
|
|
|
// TestReconciliationRemovesDamagedFiles empties a cached source file and a
|
|
// cached variant file, or cuts them to half their size, as a crash or a
|
|
// restored snapshot could. It checks that the reconciliation pass removes
|
|
// each file and its row, with a warning naming the file.
|
|
func TestReconciliationRemovesDamagedFiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
content := []byte("the whole cached image")
|
|
|
|
for name, size := range map[string]int64{
|
|
"empty": 0,
|
|
"partial": int64(len(content) / 2),
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
sourceKey := storeEvictionTestSource(t, cache, testHostCDN, testPathCat, content)
|
|
storeEvictionTestVariant(t, cache, testVariantKeyOne, content)
|
|
|
|
sourcePath := cache.srcContent.keyToPath(sourceKey)
|
|
variantPath := cache.variants.keyToPath(testVariantKeyOne)
|
|
|
|
for _, path := range []string{sourcePath, variantPath} {
|
|
err := os.Truncate(path, size)
|
|
if err != nil {
|
|
t.Fatalf("cutting %s to %d bytes: %v", path, size, err)
|
|
}
|
|
}
|
|
|
|
logs := reconcileAsAtStartup(t, cache)
|
|
|
|
assertWarningNames(t, logs, sourcePath)
|
|
assertWarningNames(t, logs, variantPath)
|
|
|
|
if cache.srcContent.Exists(sourceKey) {
|
|
t.Error("the damaged source file is still on disk")
|
|
}
|
|
|
|
if cache.variants.Exists(testVariantKeyOne) {
|
|
t.Error("the damaged variant file is still on disk")
|
|
}
|
|
|
|
rows := countRows(t, cache,
|
|
`SELECT COUNT(*) FROM source_content WHERE source_key = ?`,
|
|
string(sourceKey))
|
|
if rows != 0 {
|
|
t.Errorf("the damaged source file kept %d rows, want 0", rows)
|
|
}
|
|
|
|
rows = countRows(t, cache,
|
|
`SELECT COUNT(*) FROM variant_content WHERE cache_key = ?`,
|
|
string(testVariantKeyOne))
|
|
if rows != 0 {
|
|
t.Errorf("the damaged variant file kept %d rows, want 0", rows)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestReconciliationRemovesEmptyVariantFileWithoutRow checks that the
|
|
// reconciliation pass removes an empty variant file that has no
|
|
// accounting row, with a warning naming it, instead of adding a row for
|
|
// it that would have it served empty.
|
|
func TestReconciliationRemovesEmptyVariantFileWithoutRow(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
_, err := cache.variants.Store(testVariantKeyOne, bytes.NewReader(nil))
|
|
if err != nil {
|
|
t.Fatalf("failed to store an empty variant file: %v", err)
|
|
}
|
|
|
|
logs := reconcileAsAtStartup(t, cache)
|
|
|
|
assertWarningNames(t, logs, cache.variants.keyToPath(testVariantKeyOne))
|
|
|
|
if cache.variants.Exists(testVariantKeyOne) {
|
|
t.Error("the empty variant file is still on disk")
|
|
}
|
|
|
|
rows := countRows(t, cache,
|
|
`SELECT COUNT(*) FROM variant_content WHERE cache_key = ?`,
|
|
string(testVariantKeyOne))
|
|
if rows != 0 {
|
|
t.Errorf("the empty variant file got %d accounting rows, want 0", rows)
|
|
}
|
|
}
|
|
|
|
// TestContentStorage_StoreReplacesFileOfAnotherSize checks that a
|
|
// store over a file of another size at the source's path, such as a
|
|
// partial file a crash left, replaces it with the content.
|
|
func TestContentStorage_StoreReplacesFileOfAnotherSize(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
storage, err := NewContentStorage(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewContentStorage() error = %v", err)
|
|
}
|
|
|
|
content := []byte("the whole source image")
|
|
key := imagerequest.Source{Host: testHostCDN, Path: testPathCat}.Key()
|
|
path := storage.keyToPath(key)
|
|
|
|
err = os.MkdirAll(filepath.Dir(path), StorageDirPerm)
|
|
if err != nil {
|
|
t.Fatalf("creating the source's directory: %v", err)
|
|
}
|
|
|
|
err = os.WriteFile(path, content[:5], StorageFilePerm)
|
|
if err != nil {
|
|
t.Fatalf("writing a partial file: %v", err)
|
|
}
|
|
|
|
_, err = storage.Store(key, content)
|
|
if err != nil {
|
|
t.Fatalf("Store() error = %v", err)
|
|
}
|
|
|
|
stored, _, err := storage.LoadWithSize(key)
|
|
if err != nil {
|
|
t.Fatalf("LoadWithSize() error = %v", err)
|
|
}
|
|
|
|
defer func() { _ = stored.Close() }()
|
|
|
|
got, err := io.ReadAll(stored)
|
|
if err != nil {
|
|
t.Fatalf("reading the stored content: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(got, content) {
|
|
t.Errorf("stored file = %q, want %q", got, content)
|
|
}
|
|
}
|