11package org .opentripplanner .ext .carpooling .service ;
22
33import static org .junit .jupiter .api .Assertions .assertEquals ;
4+ import static org .junit .jupiter .api .Assertions .assertNull ;
45import static org .junit .jupiter .api .Assertions .assertTrue ;
5- import static org .opentripplanner .ext .carpooling .service .DefaultCarpoolingService .LEG_DURATION_SLACK ;
66import static org .opentripplanner .ext .carpooling .service .DefaultCarpoolingService .MAX_SEARCH_DURATION_FOR_NEARBY_STOPS_FOR_ACCESS_EGRESS ;
77
88import java .time .Duration ;
1616import org .opentripplanner .ext .carpooling .model .CarpoolStop ;
1717import org .opentripplanner .ext .carpooling .model .CarpoolTrip ;
1818import org .opentripplanner .ext .carpooling .model .CarpoolTripBuilder ;
19+ import org .opentripplanner .ext .carpooling .updater .CarpoolSiriMapper ;
1920import org .opentripplanner .street .geometry .WgsCoordinate ;
2021
2122/**
22- * Unit tests for {@link DefaultCarpoolingService#driverLegTreeLimits}: each leg gets
23- * {@code leg + slack + smallestDownstreamBudget}, multi-leg trips get per-leg limits well below
24- * the whole-trip limit, and an incomplete or non-monotonic timeline falls back to whole-trip
25- * sizing for every leg.
23+ * Unit tests for {@link DefaultCarpoolingService#driverLegTreeLimits} and
24+ * {@link DefaultCarpoolingService#scheduledLegDurations}: each leg gets
25+ * {@code leg + slack + smallestDownstreamBudget} capped at
26+ * {@link org.opentripplanner.ext.carpooling.updater.CarpoolSiriMapper#MAX_TRIP_DURATION}, multi-leg
27+ * trips get per-leg limits well below the whole-trip limit, and an incomplete or non-monotonic
28+ * schedule yields no scheduled durations (signalling the caller to fall back to whole-trip sizing).
29+ * The leg durations here come from the SIRI schedule; in production they are OTP's own routed
30+ * durations (see {@link DefaultCarpoolingService#resolveLegDurations}), but the limit arithmetic is
31+ * identical.
2632 */
2733class DefaultCarpoolingServiceTreeLimitTest {
2834
@@ -33,11 +39,26 @@ class DefaultCarpoolingServiceTreeLimitTest {
3339
3440 private static int idCounter = 0 ;
3541
42+ /** Mirrors the small slack {@link DefaultCarpoolingService#driverLegTreeLimits} adds per leg. */
43+ private static final Duration SLACK = Duration .ofMinutes (1 );
44+
3645 /**
3746 * Matches the production formula: leg + flat slack + smallest downstream deviation budget.
3847 */
3948 private static Duration expectedLimit (Duration leg , Duration allowance ) {
40- return leg .plus (LEG_DURATION_SLACK ).plus (allowance );
49+ return leg .plus (SLACK ).plus (allowance );
50+ }
51+
52+ /**
53+ * Sizes the trip's legs from its SIRI schedule, exercising the same
54+ * {@link DefaultCarpoolingService#driverLegTreeLimits} arithmetic production uses with OTP's
55+ * routed durations.
56+ */
57+ private static Duration [] limitsFromSchedule (CarpoolTrip trip ) {
58+ return DefaultCarpoolingService .driverLegTreeLimits (
59+ trip ,
60+ DefaultCarpoolingService .scheduledLegDurations (trip )
61+ );
4162 }
4263
4364 @ Test
@@ -50,7 +71,7 @@ void sizesEachLegIndependentlyAndWellBelowWholeTrip() {
5071 destinationStop (BASE .plusMinutes (160 ), BUDGET )
5172 );
5273
53- var limits = DefaultCarpoolingService . driverLegTreeLimits (trip );
74+ var limits = limitsFromSchedule (trip );
5475
5576 var leg0 = expectedLimit (Duration .ofMinutes (60 ), BUDGET );
5677 var leg1 = expectedLimit (Duration .ofMinutes (100 ), BUDGET );
@@ -77,12 +98,12 @@ void usesSmallestDownstreamBudgetAsDetourAllowance() {
7798 destinationStop (BASE .plusMinutes (120 ), Duration .ofMinutes (40 ))
7899 );
79100
80- var limits = DefaultCarpoolingService . driverLegTreeLimits (trip );
101+ var limits = limitsFromSchedule (trip );
81102
82- // Leg 0: allowance min(5, 40) = 5 → 60 + 15 + 5 = 80 ; the origin's budget never participates.
83- assertEquals (Duration .ofMinutes (80 ), limits [0 ]);
84- // Leg 1 only delays the destination: allowance 40 → 60 + 15 + 40 = 115 .
85- assertEquals (Duration .ofMinutes (115 ), limits [1 ]);
103+ // Leg 0: allowance min(5, 40) = 5; the origin's budget never participates.
104+ assertEquals (expectedLimit ( Duration .ofMinutes (60 ), Duration . ofMinutes ( 5 ) ), limits [0 ]);
105+ // Leg 1 only delays the destination: allowance 40.
106+ assertEquals (expectedLimit ( Duration .ofMinutes (60 ), Duration . ofMinutes ( 40 ) ), limits [1 ]);
86107 }
87108
88109 @ Test
@@ -96,7 +117,7 @@ void sizesShortLegsWellBelowNearbyStopRadius() {
96117 destinationStop (BASE .plusMinutes (10 ), Duration .ofMinutes (2 ))
97118 );
98119
99- var limits = DefaultCarpoolingService . driverLegTreeLimits (trip );
120+ var limits = limitsFromSchedule (trip );
100121
101122 var leg = expectedLimit (Duration .ofMinutes (5 ), Duration .ofMinutes (2 ));
102123 assertEquals (leg , limits [0 ]);
@@ -116,7 +137,7 @@ void usesAimedArrivalWhenExpectedMissing() {
116137 destinationStop (BASE .plusMinutes (90 ), BUDGET )
117138 );
118139
119- var limits = DefaultCarpoolingService . driverLegTreeLimits (trip );
140+ var limits = limitsFromSchedule (trip );
120141
121142 assertEquals (expectedLimit (Duration .ofMinutes (50 ), BUDGET ), limits [0 ]);
122143 assertEquals (expectedLimit (Duration .ofMinutes (40 ), BUDGET ), limits [1 ]);
@@ -133,7 +154,7 @@ void destinationLatestArrivalDoesNotInflateLastLeg() {
133154 destinationStopWithTimes (BASE .plusMinutes (120 ), BASE .plusMinutes (160 ), BUDGET )
134155 );
135156
136- var limits = DefaultCarpoolingService . driverLegTreeLimits (trip );
157+ var limits = limitsFromSchedule (trip );
137158
138159 // Last leg is the scheduled 60 min (12:00+60 → +120), not the 100 min the latest expected
139160 // arrival (+160) would give.
@@ -148,62 +169,90 @@ void twoWaypointTripSizesTheSingleLegToTheWholeSpan() {
148169 destinationStop (BASE .plusMinutes (60 ), BUDGET )
149170 );
150171
151- var limits = DefaultCarpoolingService . driverLegTreeLimits (trip );
172+ var limits = limitsFromSchedule (trip );
152173
153174 var legLimit = expectedLimit (Duration .ofMinutes (60 ), BUDGET );
154175 assertEquals (1 , limits .length );
155176 assertEquals (legLimit , limits [0 ]);
156177 }
157178
158179 @ Test
159- void fallsBackToWholeTripWhenIntermediateTimeMissing () {
180+ void capsEachLegLimitAtMaxTripDuration () {
181+ // An inconsistent trip whose leg travel time already exceeds the trip cap (e.g. one that
182+ // slipped past the mapper) must not size a tree beyond MAX_TRIP_DURATION; otherwise a single
183+ // request could expand a multi-hour street tree.
184+ var trip = trip (
185+ BASE .plusMinutes (200 ),
186+ originStop (BUDGET ),
187+ destinationStop (BASE .plusMinutes (200 ), BUDGET )
188+ );
189+
190+ var limits = DefaultCarpoolingService .driverLegTreeLimits (
191+ trip ,
192+ new Duration [] { Duration .ofHours (4 ) }
193+ );
194+
195+ assertEquals (CarpoolSiriMapper .MAX_TRIP_DURATION , limits [0 ]);
196+ }
197+
198+ @ Test
199+ void doesNotCapLegLimitsThatStayWithinMaxTripDuration () {
200+ // A leg comfortably under the cap keeps its full duration + slack + allowance — the cap only
201+ // bites the extreme case above.
202+ var trip = trip (
203+ BASE .plusMinutes (60 ),
204+ originStop (BUDGET ),
205+ destinationStop (BASE .plusMinutes (60 ), BUDGET )
206+ );
207+
208+ var limits = DefaultCarpoolingService .driverLegTreeLimits (
209+ trip ,
210+ new Duration [] { Duration .ofMinutes (30 ) }
211+ );
212+
213+ assertEquals (expectedLimit (Duration .ofMinutes (30 ), BUDGET ), limits [0 ]);
214+ assertTrue (limits [0 ].compareTo (CarpoolSiriMapper .MAX_TRIP_DURATION ) < 0 );
215+ }
216+
217+ @ Test
218+ void scheduledLegDurationsIsNullWhenIntermediateTimeMissing () {
160219 var trip = trip (
161220 BASE .plusMinutes (120 ),
162221 originStop (BUDGET ),
163222 intermediateStop (null , BUDGET ),
164223 destinationStop (BASE .plusMinutes (120 ), BUDGET )
165224 );
166225
167- var limits = DefaultCarpoolingService .driverLegTreeLimits (trip );
168-
169- var wholeTrip = expectedLimit (Duration .ofMinutes (120 ), BUDGET );
170- assertEquals (wholeTrip , limits [0 ]);
171- assertEquals (wholeTrip , limits [1 ]);
226+ // No scheduled durations → the caller sizes every leg from the whole-trip span instead.
227+ assertNull (DefaultCarpoolingService .scheduledLegDurations (trip ));
172228 }
173229
174230 @ Test
175- void fallsBackToWholeTripWhenDestinationTimeMissing () {
176- // The destination is not special-cased: its missing arrival falls back like any other stop's.
231+ void scheduledLegDurationsIsNullWhenDestinationTimeMissing () {
232+ // The destination is not special-cased: its missing arrival yields no scheduled durations,
233+ // like any other stop's.
177234 var trip = trip (
178235 BASE .plusMinutes (120 ),
179236 originStop (BUDGET ),
180237 intermediateStop (BASE .plusMinutes (60 ), BUDGET ),
181238 destinationStop (null , BUDGET )
182239 );
183240
184- var limits = DefaultCarpoolingService .driverLegTreeLimits (trip );
185-
186- var wholeTrip = expectedLimit (Duration .ofMinutes (120 ), BUDGET );
187- assertEquals (wholeTrip , limits [0 ]);
188- assertEquals (wholeTrip , limits [1 ]);
241+ assertNull (DefaultCarpoolingService .scheduledLegDurations (trip ));
189242 }
190243
191244 @ Test
192- void fallsBackToWholeTripWhenTimelineNotMonotonic () {
245+ void scheduledLegDurationsIsNullWhenTimelineNotMonotonic () {
193246 // Intermediate arrival is before the trip start — an impossible timeline that must not yield a
194- // negative leg; the whole-trip span sizes every leg instead.
247+ // negative leg; the caller sizes every leg from the whole-trip span instead.
195248 var trip = trip (
196249 BASE .plusMinutes (120 ),
197250 originStop (BUDGET ),
198251 intermediateStop (BASE .minusMinutes (10 ), BUDGET ),
199252 destinationStop (BASE .plusMinutes (120 ), BUDGET )
200253 );
201254
202- var limits = DefaultCarpoolingService .driverLegTreeLimits (trip );
203-
204- var wholeTrip = expectedLimit (Duration .ofMinutes (120 ), BUDGET );
205- assertEquals (wholeTrip , limits [0 ]);
206- assertEquals (wholeTrip , limits [1 ]);
255+ assertNull (DefaultCarpoolingService .scheduledLegDurations (trip ));
207256 }
208257
209258 private static CarpoolTrip trip (ZonedDateTime endTime , CarpoolStop ... stops ) {
0 commit comments