check / check (push) Waiting to run
The five counts the cache_stats table held are now Prometheus counters in memory, as fields of the cache. NewCache makes them and registers them on the registry it is given. pixad gives it Prometheus' default registry, which /metrics serves; each test gives it a new one. The Increment methods add to their cache's counters, so counting makes no database call. cache_stats, Cache.Stats, Service.Stats, CacheStats and the pending counts are gone, with their tests. The background goroutine now writes only access times, and its names say so: StartPendingAccessTimeWrites, StopPendingAccessTimeWrites and pendingaccesstimes.go. The two stop tests whose write under way was a count now use access times. Model: opus-5-5
244 lines
6.2 KiB
Go
244 lines
6.2 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image/color"
|
|
"io"
|
|
"io/fs"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/pixa/internal/httpfetcher"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// TestService_Get_CountsStats walks Get through a miss that fetches the
|
|
// source, a hit, a miss that reuses the cached source, and two misses whose
|
|
// source cannot be used, checking every counter of the cache after each.
|
|
func TestService_Get_CountsStats(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, fixtures := SetupTestService(t)
|
|
|
|
// NewTestFS builds the same files the test service's fetcher serves.
|
|
testFS, _ := NewTestFS(t)
|
|
|
|
photo, err := fs.ReadFile(testFS, fixtures.GoodHostJPEG)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fake, err := fs.ReadFile(testFS, fixtures.InvalidFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
photoBytes, fakeBytes := int64(len(photo)), int64(len(fake))
|
|
|
|
// want is hits, misses, upstream fetches, upstream bytes, transforms.
|
|
steps := []struct {
|
|
name string
|
|
path string
|
|
size int
|
|
wantErr bool
|
|
want counterValues
|
|
}{
|
|
{"miss that fetches the source", testPathPhoto, 50, false,
|
|
counterValues{0, 1, 1, photoBytes, 1}},
|
|
{"hit", testPathPhoto, 50, false,
|
|
counterValues{1, 1, 1, photoBytes, 1}},
|
|
{"miss that reuses the cached source", testPathPhoto, 25, false,
|
|
counterValues{1, 2, 1, photoBytes, 2}},
|
|
{"miss whose source fails the magic byte check", "/images/fake.jpg", 50, true,
|
|
counterValues{1, 3, 2, photoBytes + fakeBytes, 2}},
|
|
{"miss whose source is not found", "/images/nonexistent.jpg", 50, true,
|
|
counterValues{1, 4, 2, photoBytes + fakeBytes, 2}},
|
|
}
|
|
|
|
for _, step := range steps {
|
|
resp, err := svc.Get(t.Context(), &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: fixtures.GoodHost,
|
|
Path: step.path,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: step.size, Height: step.size},
|
|
Format: imagerequest.FormatJPEG,
|
|
Quality: 85,
|
|
FitMode: imagerequest.FitCover,
|
|
},
|
|
})
|
|
if (err != nil) != step.wantErr {
|
|
t.Fatalf("%s: Get() error = %v, want error %t", step.name, err, step.wantErr)
|
|
}
|
|
|
|
if err == nil {
|
|
_ = resp.Content.Close()
|
|
}
|
|
|
|
got := readCounters(svc.cache)
|
|
if got != step.want {
|
|
t.Fatalf("after the %s: counters = %+v, want %+v", step.name, got, step.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// fakeUpstream answers every fetch with itself as a JPEG body. The body
|
|
// serves data, then calls cancel, when set, and returns err; io.EOF ends
|
|
// the body normally.
|
|
type fakeUpstream struct {
|
|
data *bytes.Reader
|
|
cancel context.CancelFunc
|
|
err error
|
|
}
|
|
|
|
func (u *fakeUpstream) Fetch(
|
|
context.Context, string,
|
|
) (*httpfetcher.FetchResult, error) {
|
|
return &httpfetcher.FetchResult{
|
|
Content: io.NopCloser(u),
|
|
ContentLength: -1,
|
|
ContentType: testContentTypeJPEG,
|
|
}, nil
|
|
}
|
|
|
|
func (u *fakeUpstream) Read(p []byte) (int, error) {
|
|
if u.data.Len() > 0 {
|
|
return u.data.Read(p)
|
|
}
|
|
|
|
if u.cancel != nil {
|
|
u.cancel()
|
|
}
|
|
|
|
return 0, u.err
|
|
}
|
|
|
|
// TestService_Get_CountsInterruptedMisses checks every counter of the cache
|
|
// after a miss whose request context ends during or after the upstream
|
|
// fetch, and after a miss whose upstream body is over the size limit.
|
|
func TestService_Get_CountsInterruptedMisses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
photo := generateTestJPEG(t, 100, 100, color.RGBA{255, 0, 0, 255})
|
|
half := len(photo) / 2
|
|
|
|
// want is hits, misses, upstream fetches, upstream bytes, transforms.
|
|
tests := []struct {
|
|
name string
|
|
served int // bytes of the photo the upstream body serves
|
|
cancel bool // whether the body then ends the request context
|
|
readErr error // what the body then returns
|
|
wantErr bool
|
|
want counterValues
|
|
}{
|
|
{"request context ends during the fetch", half, true, context.Canceled, true,
|
|
counterValues{0, 1, 1, int64(half), 0}},
|
|
{"request context ends after the fetch", len(photo), true, io.EOF, false,
|
|
counterValues{0, 1, 1, int64(len(photo)), 1}},
|
|
{"upstream body over the size limit", half, false,
|
|
httpfetcher.ErrResponseTooLarge, true,
|
|
counterValues{0, 1, 1, int64(half), 0}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, fixtures := SetupTestService(t)
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
upstream := &fakeUpstream{
|
|
data: bytes.NewReader(photo[:tc.served]),
|
|
err: tc.readErr,
|
|
}
|
|
if tc.cancel {
|
|
upstream.cancel = cancel
|
|
}
|
|
|
|
svc.fetcher = upstream
|
|
|
|
resp, err := svc.Get(ctx, &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: fixtures.GoodHost,
|
|
Path: testPathPhoto,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 50, Height: 50},
|
|
Format: imagerequest.FormatJPEG,
|
|
Quality: 85,
|
|
FitMode: imagerequest.FitCover,
|
|
},
|
|
})
|
|
t.Logf("Get() error = %v", err)
|
|
|
|
if (err != nil) != tc.wantErr {
|
|
t.Fatalf("Get() error = %v, want error %t", err, tc.wantErr)
|
|
}
|
|
|
|
if err == nil {
|
|
_ = resp.Content.Close()
|
|
}
|
|
|
|
got := readCounters(svc.cache)
|
|
if got != tc.want {
|
|
t.Errorf("counters = %+v, want %+v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestService_Get_CountsHitAfterRequestEnds checks every counter of the
|
|
// cache after a hit served with a request context that has already ended:
|
|
// only the hit count moves.
|
|
func TestService_Get_CountsHitAfterRequestEnds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, fixtures := SetupTestService(t)
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: fixtures.GoodHost,
|
|
Path: testPathPhoto,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 50, Height: 50},
|
|
Format: imagerequest.FormatJPEG,
|
|
Quality: 85,
|
|
FitMode: imagerequest.FitCover,
|
|
},
|
|
}
|
|
|
|
// A first request caches the variant.
|
|
resp, err := svc.Get(t.Context(), req)
|
|
if err != nil {
|
|
t.Fatalf("first Get() error = %v", err)
|
|
}
|
|
|
|
_ = resp.Content.Close()
|
|
|
|
want := readCounters(svc.cache)
|
|
want.hitCount++
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
|
|
resp, err = svc.Get(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("Get() with an ended request context: error = %v", err)
|
|
}
|
|
|
|
_ = resp.Content.Close()
|
|
|
|
if resp.CacheStatus != CacheHit {
|
|
t.Fatalf("CacheStatus = %v, want %v", resp.CacheStatus, CacheHit)
|
|
}
|
|
|
|
got := readCounters(svc.cache)
|
|
if got != want {
|
|
t.Errorf("counters = %+v, want %+v", got, want)
|
|
}
|
|
}
|