Ctrl K
ComunidadGuías…

tariff/embed_test.go

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

Archivo original ↗Preguntar a KiBot
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")
}