Files
ricycleplanner/main_test.go
2025-05-01 23:12:35 +02:00

593 lines
15 KiB
Go

package main
import (
"github.com/tkrajina/gpxgo/gpx"
"math"
"testing"
)
func TestParseMultiStops(t *testing.T) {
tests := []struct {
name string
input string
expected []MultiStop
}{
{
name: "Empty input",
input: "",
expected: []MultiStop{},
},
{
name: "Single stop",
input: "59.3293,18.0686:1",
expected: []MultiStop{
{Lat: 59.3293, Lon: 18.0686, Nights: 1},
},
},
{
name: "Multiple stops",
input: "59.3293,18.0686:1;60.1282,18.6435:2",
expected: []MultiStop{
{Lat: 59.3293, Lon: 18.0686, Nights: 1},
{Lat: 60.1282, Lon: 18.6435, Nights: 2},
},
},
{
name: "Invalid format",
input: "invalid",
expected: []MultiStop{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parseMultiStops(tt.input)
if len(result) != len(tt.expected) {
t.Errorf("Expected %d stops, got %d", len(tt.expected), len(result))
return
}
for i, stop := range result {
if stop.Lat != tt.expected[i].Lat || stop.Lon != tt.expected[i].Lon || stop.Nights != tt.expected[i].Nights {
t.Errorf("Stop %d mismatch: expected %+v, got %+v", i, tt.expected[i], stop)
}
}
})
}
}
func TestHaversine(t *testing.T) {
tests := []struct {
name string
lat1 float64
lon1 float64
lat2 float64
lon2 float64
expected float64
}{
{
name: "Same point",
lat1: 59.3293,
lon1: 18.0686,
lat2: 59.3293,
lon2: 18.0686,
expected: 0,
},
{
name: "Stockholm to Uppsala",
lat1: 59.3293,
lon1: 18.0686,
lat2: 59.8586,
lon2: 17.6389,
expected: 63.63, // Approximate distance in km
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := haversine(tt.lat1, tt.lon1, tt.lat2, tt.lon2)
// Allow for small floating point differences
if math.Abs(result-tt.expected) > 0.5 {
t.Errorf("Expected distance of %.2f km, got %.2f km", tt.expected, result)
}
})
}
}
func TestNormalizeCutIndexes(t *testing.T) {
tests := []struct {
name string
cutIndexes []int
days int
totalPoints int
expected []int
}{
{
name: "No cuts",
cutIndexes: []int{},
days: 5,
totalPoints: 100,
expected: []int{},
},
{
name: "Fewer cuts than days",
cutIndexes: []int{25, 50, 75},
days: 5,
totalPoints: 100,
expected: []int{25, 50, 75},
},
{
name: "More cuts than days",
cutIndexes: []int{20, 40, 60, 80, 99},
days: 3,
totalPoints: 100,
expected: []int{20, 40, 99},
},
{
name: "Duplicate cuts (rest days)",
cutIndexes: []int{25, 25, 50, 75},
days: 5,
totalPoints: 100,
expected: []int{25, 25, 50, 75},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := normalizeCutIndexes(tt.cutIndexes, tt.days, tt.totalPoints)
if len(result) != len(tt.expected) {
t.Errorf("Expected %d cuts, got %d", len(tt.expected), len(result))
return
}
for i, cut := range result {
if cut != tt.expected[i] {
t.Errorf("Cut %d mismatch: expected %d, got %d", i, tt.expected[i], cut)
}
}
})
}
}
func TestCreateSegment(t *testing.T) {
// Create test points
points := []TrackPoint{
{Index: 0, Distance: 0, Elevation: 0},
{Index: 1, Distance: 10, Elevation: 100},
{Index: 2, Distance: 20, Elevation: 200},
{Index: 3, Distance: 30, Elevation: 300},
{Index: 4, Distance: 40, Elevation: 400},
{Index: 5, Distance: 50, Elevation: 500},
}
tests := []struct {
name string
start int
end int
cutIndexes []int
segmentIndex int
expectedLen int
}{
{
name: "Normal segment",
start: 0,
end: 3,
cutIndexes: []int{3, 5},
segmentIndex: 0,
expectedLen: 3, // Points 0, 1, 2
},
{
name: "Forced stop (end <= start)",
start: 3,
end: 3,
cutIndexes: []int{3, 5},
segmentIndex: 0,
expectedLen: 2, // Points 3, 4 (halfway to the next cut)
},
{
name: "Last segment",
start: 3,
end: 5,
cutIndexes: []int{3, 5},
segmentIndex: 1,
expectedLen: 2, // Points 3, 4
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := createSegment(points, tt.start, tt.end, tt.cutIndexes, tt.segmentIndex)
if len(result) != tt.expectedLen {
t.Errorf("Expected segment length %d, got %d", tt.expectedLen, len(result))
}
})
}
}
func TestCreateDaySegment(t *testing.T) {
// Create test points
points := []TrackPoint{
{Index: 0, Distance: 0, Elevation: 0},
{Index: 1, Distance: 10, Elevation: 100},
{Index: 2, Distance: 20, Elevation: 200},
}
dayNumber := 1
elevFactor := 4.0
forestRadius := 3
resupplyRadius := 500
// This test will not query for forest or resupply points
// since that would require network access. We're just testing the
// calculation of distance, elevation, and effort.
segment := createDaySegment(points, dayNumber, elevFactor, forestRadius, resupplyRadius, false)
if segment.DayNumber != dayNumber {
t.Errorf("Expected day number %d, got %d", dayNumber, segment.DayNumber)
}
expectedDist := 20.0 // Last point distance - first point distance
if math.Abs(segment.Distance-expectedDist) > 0.1 {
t.Errorf("Expected distance %.1f, got %.1f", expectedDist, segment.Distance)
}
expectedElev := 200.0 // Last point elevation - first point elevation
if math.Abs(segment.Elevation-expectedElev) > 0.1 {
t.Errorf("Expected elevation %.1f, got %.1f", expectedElev, segment.Elevation)
}
expectedEffort := expectedDist + (expectedElev / 100.0 * elevFactor)
if math.Abs(segment.Effort-expectedEffort) > 0.1 {
t.Errorf("Expected effort %.1f, got %.1f", expectedEffort, segment.Effort)
}
}
func TestCalculateCutIndexes(t *testing.T) {
// Create test route data
points := []TrackPoint{
{Index: 0, Distance: 0, Elevation: 0},
{Index: 1, Distance: 25, Elevation: 250}, // Effort: 25 + (250/100*4) = 35
{Index: 2, Distance: 50, Elevation: 500}, // Effort: 50 + (500/100*4) = 70
{Index: 3, Distance: 75, Elevation: 750}, // Effort: 75 + (750/100*4) = 105
{Index: 4, Distance: 100, Elevation: 1000}, // Effort: 100 + (1000/100*4) = 140
}
routeData := RouteData{
Points: points,
TotalDist: 100,
TotalElev: 1000,
TotalEffort: 140,
}
tests := []struct {
name string
days int
elevFactor float64
multiStops []MultiStop
expected []int
}{
{
name: "2 days, no stops",
days: 2,
elevFactor: 4.0,
multiStops: []MultiStop{},
expected: []int{2, 4}, // Cut at index 2 (effort 70) and 4 (end)
},
{
name: "3 days, no stops",
days: 3,
elevFactor: 4.0,
multiStops: []MultiStop{},
expected: []int{1, 3, 4}, // Cut at index 1, 3, and 4 (end)
},
{
name: "3 days, with one multi-stop",
days: 3,
elevFactor: 4.0,
multiStops: []MultiStop{
{Lat: 0, Lon: 0, Nights: 1}, // This will match point 0
},
expected: []int{0, 1, 4}, // Cut at index 0 (forced stop), 1, and 4 (end)
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calculateCutIndexes(routeData, tt.days, tt.elevFactor, tt.multiStops)
if len(result) != len(tt.expected) {
t.Errorf("Expected %d cuts, got %d", len(tt.expected), len(result))
return
}
for i, cut := range result {
if cut != tt.expected[i] {
t.Errorf("Cut %d mismatch: expected %d, got %d", i, tt.expected[i], cut)
}
}
})
}
}
// New tests for refactored functions
func TestFindForcedStopIndexes(t *testing.T) {
// Create test points with latitude and longitude
points := []TrackPoint{
{Index: 0, Point: gpx.GPXPoint{Point: gpx.Point{Latitude: 59.3293, Longitude: 18.0686}}},
{Index: 1, Point: gpx.GPXPoint{Point: gpx.Point{Latitude: 59.8586, Longitude: 17.6389}}},
{Index: 2, Point: gpx.GPXPoint{Point: gpx.Point{Latitude: 60.1282, Longitude: 18.6435}}},
}
tests := []struct {
name string
points []TrackPoint
multiStops []MultiStop
expected []int
}{
{
name: "No stops",
points: points,
multiStops: []MultiStop{},
expected: []int{},
},
{
name: "One stop matching first point",
points: points,
multiStops: []MultiStop{
{Lat: 59.3293, Lon: 18.0686, Nights: 1}, // Matches point 0
},
expected: []int{0},
},
{
name: "One stop with multiple nights",
points: points,
multiStops: []MultiStop{
{Lat: 59.8586, Lon: 17.6389, Nights: 2}, // Matches point 1
},
expected: []int{1, 1}, // Duplicated for 2 nights
},
{
name: "Multiple stops",
points: points,
multiStops: []MultiStop{
{Lat: 59.3293, Lon: 18.0686, Nights: 1}, // Matches point 0
{Lat: 60.1282, Lon: 18.6435, Nights: 1}, // Matches point 2
},
expected: []int{0, 2},
},
{
name: "Stop too far from any point",
points: points,
multiStops: []MultiStop{
{Lat: 55.0, Lon: 15.0, Nights: 1}, // Far from all points
},
expected: []int{}, // No match within 2km
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := findForcedStopIndexes(tt.points, tt.multiStops)
if len(result) != len(tt.expected) {
t.Errorf("Expected %d forced stops, got %d", len(tt.expected), len(result))
return
}
for i, idx := range result {
if idx != tt.expected[i] {
t.Errorf("Forced stop %d mismatch: expected %d, got %d", i, tt.expected[i], idx)
}
}
})
}
}
func TestHandleTooManyForcedStops(t *testing.T) {
tests := []struct {
name string
forcedStopIndexes []int
days int
lastPointIndex int
totalPoints int
expected []int
}{
{
name: "Fewer forced stops than days",
forcedStopIndexes: []int{25, 50},
days: 4,
lastPointIndex: 99,
totalPoints: 100,
expected: []int{25, 50, 99},
},
{
name: "More forced stops than days",
forcedStopIndexes: []int{20, 40, 60, 80},
days: 3,
lastPointIndex: 99,
totalPoints: 100,
expected: []int{20, 40, 99}, // Only first (days-1) forced stops are kept
},
{
name: "Equal forced stops and days",
forcedStopIndexes: []int{33, 66},
days: 3,
lastPointIndex: 99,
totalPoints: 100,
expected: []int{33, 66, 99},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := handleTooManyForcedStops(tt.forcedStopIndexes, tt.days, tt.lastPointIndex, tt.totalPoints)
if len(result) != len(tt.expected) {
t.Errorf("Expected %d cuts, got %d", len(tt.expected), len(result))
return
}
for i, cut := range result {
if cut != tt.expected[i] {
t.Errorf("Cut %d mismatch: expected %d, got %d", i, tt.expected[i], cut)
}
}
})
}
}
func TestCreateSegmentsBetweenStops(t *testing.T) {
// Create test points
points := []TrackPoint{
{Index: 0, Distance: 0, Elevation: 0},
{Index: 1, Distance: 10, Elevation: 100},
{Index: 2, Distance: 20, Elevation: 200},
{Index: 3, Distance: 30, Elevation: 300},
{Index: 4, Distance: 40, Elevation: 400},
}
tests := []struct {
name string
points []TrackPoint
allStops []int
elevFactor float64
expected []struct {
start, end int
effort float64
}
}{
{
name: "No stops",
points: points,
allStops: []int{},
elevFactor: 4.0,
expected: []struct {
start, end int
effort float64
}{},
},
{
name: "One segment",
points: points,
allStops: []int{0, 4},
elevFactor: 4.0,
expected: []struct {
start, end int
effort float64
}{
{0, 4, 56.0}, // Effort: 40 + (400/100*4) = 56
},
},
{
name: "Multiple segments",
points: points,
allStops: []int{0, 2, 4},
elevFactor: 4.0,
expected: []struct {
start, end int
effort float64
}{
{0, 2, 28.0}, // Effort: 20 + (200/100*4) = 28
{2, 4, 42.0}, // Effort: (30-20) + ((300-200)/100*4) + (40-30) + ((400-300)/100*4) = 42
},
},
{
name: "Duplicate stops (rest days)",
points: points,
allStops: []int{0, 2, 2, 4},
elevFactor: 4.0,
expected: []struct {
start, end int
effort float64
}{
{0, 2, 28.0}, // Effort: 20 + (200/100*4) = 28
{2, 4, 42.0}, // Effort: (30-20) + ((300-200)/100*4) + (40-30) + ((400-300)/100*4) = 42
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := createSegmentsBetweenStops(tt.points, tt.allStops, tt.elevFactor)
if len(result) != len(tt.expected) {
t.Errorf("Expected %d segments, got %d", len(tt.expected), len(result))
return
}
for i, seg := range result {
if seg.start != tt.expected[i].start || seg.end != tt.expected[i].end {
t.Errorf("Segment %d bounds mismatch: expected (%d,%d), got (%d,%d)",
i, tt.expected[i].start, tt.expected[i].end, seg.start, seg.end)
}
if math.Abs(seg.effort-tt.expected[i].effort) > 0.1 {
t.Errorf("Segment %d effort mismatch: expected %.1f, got %.1f",
i, tt.expected[i].effort, seg.effort)
}
}
})
}
}
func TestHandleTooManyCuts(t *testing.T) {
tests := []struct {
name string
cutIndexes []int
forcedStopIndexes []int
days int
lastPointIndex int
expectedLen int
}{
{
name: "Fewer cuts than days",
cutIndexes: []int{25, 50},
forcedStopIndexes: []int{},
days: 4,
lastPointIndex: 99,
expectedLen: 4, // Will add cuts to reach days
},
{
name: "More cuts than days",
cutIndexes: []int{20, 40, 60, 80},
forcedStopIndexes: []int{},
days: 3,
lastPointIndex: 99,
expectedLen: 3, // Will reduce to days
},
{
name: "With forced stops",
cutIndexes: []int{20, 40, 60, 80},
forcedStopIndexes: []int{20, 60},
days: 3,
lastPointIndex: 99,
expectedLen: 3, // Will keep forced stops and last point
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := handleTooManyCuts(tt.cutIndexes, tt.forcedStopIndexes, tt.days, tt.lastPointIndex)
if len(result) != tt.expectedLen {
t.Errorf("Expected %d cuts, got %d", tt.expectedLen, len(result))
}
// Check that forced stops are included
for _, forcedIdx := range tt.forcedStopIndexes {
found := false
for _, resultIdx := range result {
if resultIdx == forcedIdx {
found = true
break
}
}
if !found {
t.Errorf("Forced stop %d not found in result", forcedIdx)
}
}
// Check that last point is included
lastPointFound := false
for _, resultIdx := range result {
if resultIdx == tt.lastPointIndex {
lastPointFound = true
break
}
}
if !lastPointFound {
t.Errorf("Last point %d not found in result", tt.lastPointIndex)
}
})
}
}