Ctrl K
ComunidadGuías…

tariff/proxy_cache_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package tariff

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/evcc-io/evcc/api"
	"github.com/evcc-io/evcc/db"
	"github.com/evcc-io/evcc/db/cache"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCachedFallback(t *testing.T) {
	require.NoError(t, db.NewInstance("sqlite", ":memory:"))

	other := map[string]any{"interval": 50 * time.Millisecond}
	key := "test-retry-" + cacheKey("test-retry", other)

	// no cache: startup fails
	retryable.setErr(errors.New("unavailable"))
	_, err := NewCachedFromConfig(context.TODO(), "test-retry", other)
	require.Error(t, err)

	// outdated cache: startup succeeds, cached rates served
	// utc for comparing cached rates after json round trip
	now := time.Now().UTC().Truncate(SlotDuration)
	stale := makeRates(now.Add(-time.Hour), SlotDuration, 8, 0)
	require.NoError(t, cache.Put(key, &cached{
		Type:    api.TariffTypePriceForecast,
		Rates:   stale,
		Updated: now.Add(-time.Hour),
	}))

	res, err := NewCachedFromConfig(context.TODO(), "test-retry", other)
	require.NoError(t, err)

	rr, err := res.Rates()
	require.NoError(t, err)
	assert.Equal(t, stale, rr)
	assert.Equal(t, api.TariffTypePriceForecast, res.Type())

	// tariff becomes available: wrapper retry creates it, live rates served and cached
	retryable.setErr(nil)
	w := res.(*cachingProxy).tariff.(*Wrapper)
	w.mu.Lock()
	w.retriedAt = time.Time{}
	w.mu.Unlock()

	live, err := res.Rates()
	require.NoError(t, err)
	assert.NotEqual(t, stale, live)

	// tariff becomes unavailable at runtime: last rates served
	retryable.setErr(api.ErrOutdated)
	rr, err = res.Rates()
	require.NoError(t, err)
	assert.Equal(t, live, rr)
}

func TestCachedFallbackElapsed(t *testing.T) {
	require.NoError(t, db.NewInstance("sqlite", ":memory:"))

	other := map[string]any{"interval": 60 * time.Millisecond}
	key := "test-retry-" + cacheKey("test-retry", other)

	// cached rates have entirely elapsed
	now := time.Now().UTC().Truncate(SlotDuration)
	elapsed := makeRates(now.Add(-2*time.Hour), SlotDuration, 4, 0)
	require.NoError(t, cache.Put(key, &cached{
		Type:    api.TariffTypePriceForecast,
		Rates:   elapsed,
		Updated: now.Add(-2 * time.Hour),
	}))

	// startup fails rather than reporting an available tariff without usable rates
	retryable.setErr(errors.New("unavailable"))
	_, err := NewCachedFromConfig(context.TODO(), "test-retry", other)
	require.Error(t, err)

	// tariff available at startup, then unavailable at runtime with elapsed cache only
	retryable.setErr(nil)
	res, err := NewCachedFromConfig(context.TODO(), "test-retry", other)
	require.NoError(t, err)

	retryable.setErr(api.ErrOutdated)
	_, err = res.Rates()
	require.Error(t, err)
}

func TestCachedFreshElapsed(t *testing.T) {
	require.NoError(t, db.NewInstance("sqlite", ":memory:"))

	other := map[string]any{"interval": time.Hour}
	key := "test-retry-" + cacheKey("test-retry", other)

	// cache updated within the interval, but its rates have entirely elapsed
	now := time.Now().UTC().Truncate(SlotDuration)
	putElapsed := func() {
		require.NoError(t, cache.Put(key, &cached{
			Type:    api.TariffTypePriceForecast,
			Rates:   makeRates(now.Add(-2*time.Hour), SlotDuration, 4, 0),
			Updated: now,
		}))
	}

	// the tariff is created instead of serving the elapsed rates
	putElapsed()
	retryable.setErr(nil)
	res, err := NewCachedFromConfig(context.TODO(), "test-retry", other)
	require.NoError(t, err)

	rr, err := res.Rates()
	require.NoError(t, err)
	require.NotEmpty(t, rr)
	assert.True(t, rr[len(rr)-1].End.After(time.Now()))

	// without a tariff to create, the elapsed rates are not served either
	putElapsed()
	retryable.setErr(errors.New("unavailable"))
	_, err = NewCachedFromConfig(context.TODO(), "test-retry", other)
	require.Error(t, err)
}

func TestCacheKeepsLeadingSlots(t *testing.T) {
	require.NoError(t, db.NewInstance("sqlite", ":memory:"))

	now := time.Now().Truncate(SlotDuration)

	// the fresh tariff's first fetch starts an hour out, the cache covers the hour before
	p := &cachingProxy{
		key:      "test-merge",
		interval: time.Hour,
		tariff:   &testTariff{typ: api.TariffTypeCo2, rates: makeRates(now.Add(time.Hour), SlotDuration, 4, 10)},
		cached:   &cached{Type: api.TariffTypeCo2, Rates: makeRates(now.Add(-time.Hour), SlotDuration, 12, 0), Updated: now.Add(-2 * time.Hour)},
	}

	rr, err := p.Rates()
	require.NoError(t, err)

	// elapsed cached slots dropped, the current hour kept, fresh rates appended
	assert.Equal(t, append(makeRates(now, SlotDuration, 4, 4), makeRates(now.Add(time.Hour), SlotDuration, 4, 10)...), rr)
}

func TestCacheInterval(t *testing.T) {
	require.NoError(t, db.NewInstance("sqlite", ":memory:"))

	p := &cachingProxy{key: "test", interval: time.Hour}
	require.NoError(t, p.cachePut(api.TariffTypeSolar, api.Rates{
		{Start: time.Now(), End: time.Now().Add(time.Hour), Value: 1},
	}))

	// within interval
	res, err := p.cacheGet()
	require.NoError(t, err)
	require.Equal(t, api.TariffTypeSolar, res.Type)

	// interval elapsed
	p.cached.Updated = time.Now().Add(-2 * time.Hour)
	_, err = p.cacheGet()
	require.Error(t, err)
}