Files
RenewCMS/test/image_variants_cache_support_test.go
T

234 lines
6.4 KiB
Go

package test
import (
domain "RenewCMS/internal/domain/image"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
"time"
)
var errUsageStoreDown = errors.New("usage store down")
type mockVariantUsageRepo struct {
mutex sync.Mutex
batches []domain.VariantUsageBatch
days map[string]int
fail error
calls int
}
var _ domain.VariantUsageRepository = &mockVariantUsageRepo{}
func newMockVariantUsageRepo() *mockVariantUsageRepo {
return &mockVariantUsageRepo{days: map[string]int{}}
}
func (m *mockVariantUsageRepo) Record(batch domain.VariantUsageBatch) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.calls++
if m.fail != nil {
return m.fail
}
m.batches = append(m.batches, batch)
for _, rejection := range batch.Rejections {
m.days[rejection.Day.Format(domain.RejectionDayLayout)] += rejection.Count
}
if !batch.PruneBefore.IsZero() {
for day := range m.days {
if day < batch.PruneBefore.Format(domain.RejectionDayLayout) {
delete(m.days, day)
}
}
}
return nil
}
func (m *mockVariantUsageRepo) RejectedSince(day time.Time) (int, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
total := 0
for stored, count := range m.days {
if stored >= day.Format(domain.RejectionDayLayout) {
total += count
}
}
return total, nil
}
func (m *mockVariantUsageRepo) recordedHits() []domain.VariantHit {
m.mutex.Lock()
defer m.mutex.Unlock()
hits := []domain.VariantHit{}
for _, batch := range m.batches {
hits = append(hits, batch.Hits...)
}
return hits
}
func (m *mockVariantUsageRepo) recordedRejections() int {
m.mutex.Lock()
defer m.mutex.Unlock()
total := 0
for _, batch := range m.batches {
for _, rejection := range batch.Rejections {
total += rejection.Count
}
}
return total
}
func (m *mockVariantUsageRepo) batchCount() int {
m.mutex.Lock()
defer m.mutex.Unlock()
return len(m.batches)
}
func (m *mockVariantUsageRepo) callCount() int {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.calls
}
type cacheFixtureImage struct {
id uint32
filename string
files int
bytes int64
}
var cacheFixtureTopImages = []cacheFixtureImage{
{1, "north-wall.jpg", 6, 6082765},
{2, "lime-plaster.jpg", 6, 5138022},
{3, "kitchen-after.webp", 5, 4613734},
{4, "kitchen-before.webp", 5, 4089446},
{5, "floorboards.jpg", 5, 3774873},
{6, "IMG_4471.jpg", 4, 3250585},
{7, "stone-wall-detail.jpg", 4, 2936012},
{8, "bathroom-tiles.png", 4, 2726297},
{9, "roof-lantern.jpg", 3, 2411724},
{10, "chimney-stack.jpg", 3, 2202009},
}
type cacheFixtureVariant struct {
name string
enabled bool
files int
bytes int64
}
var cacheFixtureVariants = []cacheFixtureVariant{
{"hero", true, 244, 88080384},
{"card", true, 318, 60817408},
{"thumb", true, 412, 22020096},
{"print", false, 74, 22020096},
{"square", true, 140, 19922944},
{"og", true, 96, 9437184},
}
const (
cacheFixtureUnusedHeroRows = 200
cacheFixtureUnusedHeroBytes = 40000000
cacheFixtureUnusedThumbRows = 112
cacheFixtureUnusedThumbSize = 2991616
)
func splitBytes(total int64, parts int) []int64 {
sizes := make([]int64, parts)
for index := range sizes {
sizes[index] = total / int64(parts)
if int64(index) < total%int64(parts) {
sizes[index]++
}
}
return sizes
}
func (e *variantEnv) seedCacheRow(variantName string, sourceName string, size int64, reference time.Time) domain.VariantFile {
cached := "fixture-" + variantName + "-" + sourceName + ".jpeg"
e.storage.seed(cached, []byte("generated"))
saved, _ := e.files.Save(domain.VariantFile{VariantName: variantName, SourceName: sourceName, CachedName: cached, ContentType: "image/jpeg", Size: size, Fingerprint: "fixture"})
e.files.setReference(saved.ID, reference)
return saved
}
func (e *variantEnv) seedDefaultCache() {
recent := e.now.AddDate(0, 0, -5)
stale := e.now.AddDate(0, 0, -31)
for _, variant := range cacheFixtureVariants {
seeded := seedVariant(e.variants, variant.name, 400, 300, domain.FitCover, domain.FormatJPEG, 80)
seeded.Enabled = variant.enabled
e.variants.variants[variant.name] = seeded
}
topBytes := map[string]int64{}
topFiles := map[string]int{}
for _, img := range cacheFixtureTopImages {
e.seedImage(img.id, img.filename)
for index, size := range splitBytes(img.bytes, img.files) {
variant := cacheFixtureVariants[index%len(cacheFixtureVariants)].name
e.seedCacheRow(variant, img.filename, size, recent)
topBytes[variant] += size
topFiles[variant]++
}
}
for _, variant := range cacheFixtureVariants {
fillers := variant.files - topFiles[variant.name]
remaining := variant.bytes - topBytes[variant.name]
sizes := e.fillerSizes(variant.name, fillers, remaining)
for index, size := range sizes {
reference := recent
if e.isFixtureStale(variant.name, index) {
reference = stale
}
e.seedCacheRow(variant.name, "filler-"+variant.name+"-"+strconv.Itoa(index), size, reference)
}
}
}
func (e *variantEnv) isFixtureStale(variantName string, index int) bool {
return (variantName == "hero" && index < cacheFixtureUnusedHeroRows) || (variantName == "thumb" && index < cacheFixtureUnusedThumbRows)
}
func (e *variantEnv) fillerSizes(variantName string, fillers int, remaining int64) []int64 {
switch variantName {
case "hero":
return append(splitBytes(cacheFixtureUnusedHeroBytes, cacheFixtureUnusedHeroRows), splitBytes(remaining-cacheFixtureUnusedHeroBytes, fillers-cacheFixtureUnusedHeroRows)...)
case "thumb":
return append(splitBytes(cacheFixtureUnusedThumbSize, cacheFixtureUnusedThumbRows), splitBytes(remaining-cacheFixtureUnusedThumbSize, fillers-cacheFixtureUnusedThumbRows)...)
default:
return splitBytes(remaining, fillers)
}
}
func (e *variantEnv) seedRejectedDays(days map[string]int) {
for day, count := range days {
e.usage.days[day] = count
}
}
func (e *variantEnv) cacheStats() (*httptest.ResponseRecorder, map[string]any) {
e.t.Helper()
recorder := e.sendAsAdmin(http.MethodGet, "/v1/image-variants/cache", "")
if recorder.Code != http.StatusOK {
return recorder, nil
}
return recorder, decodeBody(e.t, recorder)
}
func (e *variantEnv) purgeCache(body string) *httptest.ResponseRecorder {
e.t.Helper()
return e.send(http.MethodPost, "/v1/image-variants/cache/purge", body, variantOwnerId)
}
func (e *variantEnv) recentRejectedFixture() {
e.seedRejectedDays(map[string]int{"2026-10-08": 30, "2026-10-05": 7, "2026-09-20": 100})
}
func compactJSON(body *httptest.ResponseRecorder) string {
return strings.TrimSpace(body.Body.String())
}