tariff/embed_test.go
Rama master · réplica de lectura del repositorio original.
package tariff
import (
"runtime"
"testing"
"time"
"github.com/evcc-io/evcc/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTotalPriceFallback(t *testing.T) {
e := embed{Charges: 0.10, Tax: 0.19}
require.NoError(t, e.init())
got := e.totalPrice(0.20, time.Now())
assert.InDelta(t, (0.20+0.10)*1.19, got, 1e-9)
}
func TestTotalPriceMatchingZone(t *testing.T) {
e := embed{
Charges: 0.50,
ChargesZones_: []chargesZoneConfig{
{Charges: 0.10, Hours: "00:00-06:00"},
},
}
require.NoError(t, e.init())
ts := time.Date(2026, 1, 15, 2, 0, 0, 0, time.Local)
got := e.totalPrice(0.20, ts)
assert.InDelta(t, 0.20+0.10, got, 1e-9)
}
func TestTotalPriceNonMatchingZone(t *testing.T) {
e := embed{
Charges: 0.50,
ChargesZones_: []chargesZoneConfig{
{Charges: 0.10, Hours: "00:00-06:00"},
},
}
require.NoError(t, e.init())
ts := time.Date(2026, 1, 15, 12, 0, 0, 0, time.Local)
got := e.totalPrice(0.20, ts)
assert.InDelta(t, 0.20+0.50, got, 1e-9)
}
func TestTotalPriceNegativeChargesZone(t *testing.T) {
e := embed{
Charges: 0.10,
ChargesZones_: []chargesZoneConfig{
{Charges: -0.05, Hours: "10:00-12:00"},
},
}
require.NoError(t, e.init())
ts := time.Date(2026, 1, 15, 11, 0, 0, 0, time.Local)
got := e.totalPrice(0.20, ts)
assert.InDelta(t, 0.20-0.05, got, 1e-9)
}
func TestTotalPriceLastZoneWins(t *testing.T) {
e := embed{
ChargesZones_: []chargesZoneConfig{
{Charges: 0.10, Hours: "00:00-06:00"},
{Charges: 0.05, Hours: "02:00-04:00"},
},
}
require.NoError(t, e.init())
// 03:00 is covered by both zones; later entry wins
ts := time.Date(2026, 1, 15, 3, 0, 0, 0, time.Local)
assert.InDelta(t, 0.20+0.05, e.totalPrice(0.20, ts), 1e-9)
// 05:00 is only covered by the broader zone
ts = time.Date(2026, 1, 15, 5, 0, 0, 0, time.Local)
assert.InDelta(t, 0.20+0.10, e.totalPrice(0.20, ts), 1e-9)
}
func TestEffectiveChargesMonthFilter(t *testing.T) {
e := embed{
Charges: 0.20,
ChargesZones_: []chargesZoneConfig{
{Charges: 0.05, Months: "Jan-Mar,Oct-Dec", Hours: "00:00-05:00"},
},
}
require.NoError(t, e.init())
// February, in window: zone applies
ts := time.Date(2026, 2, 15, 3, 0, 0, 0, time.Local)
assert.InDelta(t, 0.05, e.effectiveCharges(ts), 1e-9)
// June, in hour window but month does not match: fallback
ts = time.Date(2026, 6, 15, 3, 0, 0, 0, time.Local)
assert.InDelta(t, 0.20, e.effectiveCharges(ts), 1e-9)
}
func TestTotalPriceFormulaSeesResolvedCharges(t *testing.T) {
e := embed{
Charges: 0.10,
ChargesZones_: []chargesZoneConfig{
{Charges: 0.30, Hours: "10:00-12:00"},
},
Formula: "(price + charges) * 2",
}
require.NoError(t, e.init())
// In zone: charges resolves to 0.30
ts := time.Date(2026, 1, 15, 11, 0, 0, 0, time.Local)
assert.InDelta(t, (0.20+0.30)*2, e.totalPrice(0.20, ts), 1e-9)
// Out of zone: falls back to base 0.10
ts = time.Date(2026, 1, 15, 14, 0, 0, 0, time.Local)
assert.InDelta(t, (0.20+0.10)*2, e.totalPrice(0.20, ts), 1e-9)
}
func TestEmbedDecodeChargesZones(t *testing.T) {
other := map[string]any{
"charges": 0.20,
"chargesZones": []map[string]any{
{"charges": 0.05, "months": "Jan-Mar", "hours": "00:00-05:00"},
{"charges": 0.30, "hours": "18:00-21:00"},
},
}
var cc struct {
embed `mapstructure:",squash"`
}
require.NoError(t, util.DecodeOther(other, &cc))
require.NoError(t, cc.embed.init())
assert.Len(t, cc.ChargesZones_, 2)
assert.InDelta(t, 0.05, cc.ChargesZones_[0].Charges, 1e-9)
assert.Equal(t, "Jan-Mar", cc.ChargesZones_[0].Months)
assert.Len(t, cc.chargesZones, 2)
}
func TestTotalPriceFormulaMemoryStable(t *testing.T) {
ts := time.Date(2026, 1, 15, 11, 0, 0, 0, time.Local)
e := embed{
Charges: 0.10,
Tax: 0.19,
Formula: "(price + charges) * (1 + tax)",
}
require.NoError(t, e.init())
sample := func() uint64 {
runtime.GC()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
return ms.HeapAlloc
}
before := int64(sample())
for i := 0; i < 10_000; i++ {
e.totalPrice(0.20, ts)
}
after := int64(sample())
runtime.KeepAlive(&e)
// Interpreter grows ~4.5KB per parsed statement; 10k calls would add ~45MB
assert.Less(t, after-before, int64(8<<20)) // 8MB
}
func TestTotalPriceFormulaPanic(t *testing.T) {
e := embed{Formula: "float64([]int{1}[int(price)])"}
require.NoError(t, e.init())
_, err := e.calc(5, 0, time.Now())
require.ErrorContains(t, err, "panic")
assert.Zero(t, e.totalPrice(5, time.Now()))
}
func TestTotalPriceFormulaFloatKind(t *testing.T) {
e := embed{Formula: "float32(price)"}
require.NoError(t, e.init())
// the formula yields float32, so the result only carries float32 precision
assert.InDelta(t, 0.20, e.totalPrice(0.20, time.Now()), 1e-6)
}
func TestTotalPriceFormulaScript(t *testing.T) {
e := embed{
Tax: 0.1,
Formula: "verkko := 0.025; if ts.Hour() < 7 || ts.Hour() >= 22 { verkko = 0.0112 }; math.Max((price + charges + tax)*1.255 + verkko, 0.0)",
}
require.NoError(t, e.init())
ts := time.Date(2026, 1, 1, 12, 0, 0, 0, time.Local)
assert.InDelta(t, (1+0.1)*1.255+0.025, e.totalPrice(1, ts), 1e-9)
ts = time.Date(2026, 1, 1, 23, 0, 0, 0, time.Local)
assert.InDelta(t, (1+0.1)*1.255+0.0112, e.totalPrice(1, ts), 1e-9)
}
func TestTotalPriceFormulaMultiline(t *testing.T) {
e := embed{Formula: "x := 2.0 // factor\nprice * x // result"}
require.NoError(t, e.init())
assert.InDelta(t, 0.4, e.totalPrice(0.2, time.Now()), 1e-9)
}
func TestTotalPriceFormulaNoTrailingExpression(t *testing.T) {
e := embed{Formula: "x := price"}
require.ErrorContains(t, e.init(), "must end with an expression")
}