From 87e29e1eb5a113fbb40d452a193219a40780c302 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Tue, 25 Aug 2026 13:17:20 +0200 Subject: Describe test rides as plain strings with a ridetest DSL Add internal/ridetest parsing "2km at 7% | 1km at -7%" sections into a ride.Ride, with Sampling, StartCoord, StartTime and StartElevation options, plus per-section hr/cad/w metrics. Replace the package-private RideBuilder in climb/difficulty tests and export FromColumnsWithMetrics so metric columns can be built outside the ride package. --- internal/ridetest/ride_test.go | 146 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 internal/ridetest/ride_test.go (limited to 'internal/ridetest/ride_test.go') diff --git a/internal/ridetest/ride_test.go b/internal/ridetest/ride_test.go new file mode 100644 index 0000000..69c084d --- /dev/null +++ b/internal/ridetest/ride_test.go @@ -0,0 +1,146 @@ +package ridetest + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRideSingleSection(t *testing.T) { + r, err := Ride("1km at 10%") + require.NoError(t, err) + + assert.Len(t, r.Distances(), 11) + assert.Equal(t, 0.0, r.Distances()[0]) + assert.Equal(t, 1000.0, r.Distances()[10]) + assert.Equal(t, 0.0, r.Elevations()[0]) + assert.Equal(t, 100.0, r.Elevations()[10]) +} + +func TestRideSectionsWithDescentAndFlat(t *testing.T) { + r, err := Ride("0.5km at 10% | 0.5km at -10%") + require.NoError(t, err) + + assert.Len(t, r.Distances(), 11) + assert.Equal(t, 1000.0, r.Distances()[10]) + assert.Equal(t, 50.0, r.Elevations()[5]) // top of the first section + assert.Equal(t, 0.0, r.Elevations()[10]) // back to start elevation +} + +func TestRideSamplingOption(t *testing.T) { + r, err := Ride("1km at 10%", Sampling(50)) + require.NoError(t, err) + + assert.Len(t, r.Distances(), 21) + assert.Equal(t, 1000.0, r.Distances()[20]) + assert.Equal(t, 5.0, r.Elevations()[1]) +} + +func TestRideStartCoordHeading(t *testing.T) { + r, err := Ride("2km at 0%", StartCoord(43.10, 5.10), Sampling(1000)) + require.NoError(t, err) + + // Coordinates advance northeast by 0.01° per km; float64 literals like + // 43.11 drift in the last bits, so compare with a small tolerance. + require.Len(t, r.Coords(), 3) + assert.InDelta(t, 43.10, r.Coords()[0].Lat, 1e-9) + assert.InDelta(t, 5.10, r.Coords()[0].Lon, 1e-9) + assert.InDelta(t, 43.11, r.Coords()[1].Lat, 1e-9) + assert.InDelta(t, 5.11, r.Coords()[1].Lon, 1e-9) + assert.InDelta(t, 43.12, r.Coords()[2].Lat, 1e-9) + assert.InDelta(t, 5.12, r.Coords()[2].Lon, 1e-9) +} + +func TestRideStartTime(t *testing.T) { + start := time.Date(2026, 8, 1, 10, 0, 0, 0, time.UTC) + r, err := Ride("1km at 0%", StartTime(start), Sampling(500)) + require.NoError(t, err) + + assert.Len(t, r.Timestamps(), 3) + assert.Equal(t, start, r.Timestamps()[0]) + assert.Equal(t, start.Add(time.Second), r.Timestamps()[1]) + assert.Equal(t, start.Add(2*time.Second), r.Timestamps()[2]) +} + +func TestRideStartElevation(t *testing.T) { + r, err := Ride("1km at 0%", StartElevation(100)) + require.NoError(t, err) + + assert.Equal(t, 100.0, r.Elevations()[0]) + assert.Equal(t, 100.0, r.Elevations()[10]) +} + +func TestRideMetricsCompleteColumns(t *testing.T) { + r, err := Ride("0.5km at 0% hr=150 cad=80 w=200 | 0.5km at 0% hr=140 cad=70 w=150") + require.NoError(t, err) + + heartRates, ok := r.HeartRates() + require.True(t, ok) + assert.Equal(t, 150.0, heartRates[0]) + assert.Equal(t, 150.0, heartRates[5]) + assert.Equal(t, 140.0, heartRates[6]) + assert.Equal(t, 140.0, heartRates[10]) + + cadences, ok := r.Cadences() + require.True(t, ok) + assert.Equal(t, 80.0, cadences[0]) + assert.Equal(t, 70.0, cadences[10]) + + powers, ok := r.Powers() + require.True(t, ok) + assert.Equal(t, 200.0, powers[0]) + assert.Equal(t, 150.0, powers[10]) +} + +func TestRideMetricsWithoutOptions(t *testing.T) { + r, err := Ride("1km at 0%") + require.NoError(t, err) + + _, ok := r.HeartRates() + assert.False(t, ok) + _, ok = r.Cadences() + assert.False(t, ok) + _, ok = r.Powers() + assert.False(t, ok) +} + +func TestRideMetricsPartialColumnFails(t *testing.T) { + _, err := Ride("0.5km at 0% hr=150 | 0.5km at 0%") + require.Error(t, err) + assert.Contains(t, err.Error(), "hr=") +} + +func TestRideParseErrors(t *testing.T) { + cases := []string{ + "", + " ", + "|", + "1km at 0% |", + "at 7%", + "1km 7%", + "0km at 7%", + "-1km at 7%", + "1km at abc%", + "xyz at 7%", + "1km away 7%", + "1km at 7% foo=1", + "1km at 7% hr=abc", + } + for _, dsl := range cases { + _, err := Ride(dsl) + require.Error(t, err, "dsl %q", dsl) + } +} + +func TestRideRejectsNonPositiveSampling(t *testing.T) { + _, err := Ride("1km at 0%", Sampling(0)) + require.Error(t, err) + _, err = Ride("1km at 0%", Sampling(-10)) + require.Error(t, err) +} + +func TestMustRidePanicsOnMalformedDSL(t *testing.T) { + assert.Panics(t, func() { MustRide("nope") }) +} -- cgit v1.3