Skip to content

Commit 5ea08d2

Browse files
authored
Merge pull request opentripplanner#7335 from entur/snapshot-mutation-tests
Add non-regression tests for snapshot mutation patterns
2 parents 8032df7 + d0ffac0 commit 5ea08d2

2 files changed

Lines changed: 190 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package org.opentripplanner.updater.trip.gtfs.moduletests.cancellation;
2+
3+
import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
import static org.junit.jupiter.api.Assertions.assertNull;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
9+
import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess;
10+
import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL;
11+
12+
import org.junit.jupiter.api.Test;
13+
import org.opentripplanner.transit.model._data.TransitTestEnvironment;
14+
import org.opentripplanner.transit.model._data.TransitTestEnvironmentBuilder;
15+
import org.opentripplanner.transit.model._data.TripInput;
16+
import org.opentripplanner.transit.model.site.RegularStop;
17+
import org.opentripplanner.transit.model.timetable.RealTimeState;
18+
import org.opentripplanner.updater.trip.GtfsRtTestHelper;
19+
import org.opentripplanner.updater.trip.RealtimeTestConstants;
20+
21+
/**
22+
* Test canceling a trip after a pattern change (skipped stop). This exercises the revert path
23+
* where a modified pattern must be cleaned up before the cancellation is applied to the
24+
* scheduled pattern.
25+
*/
26+
class CancelAfterPatternChangeTest implements RealtimeTestConstants {
27+
28+
private final TransitTestEnvironmentBuilder ENV_BUILDER = TransitTestEnvironment.of();
29+
private final RegularStop STOP_A = ENV_BUILDER.stop(STOP_A_ID);
30+
private final RegularStop STOP_B = ENV_BUILDER.stop(STOP_B_ID);
31+
private final RegularStop STOP_C = ENV_BUILDER.stop(STOP_C_ID);
32+
33+
private final TripInput TRIP_INPUT = TripInput.of(TRIP_1_ID)
34+
.addStop(STOP_A, "0:01:00", "0:01:01")
35+
.addStop(STOP_B, "0:01:10", "0:01:11")
36+
.addStop(STOP_C, "0:01:20", "0:01:21");
37+
38+
/**
39+
* First skip a stop (creating a modified pattern), then cancel the trip. The cancellation
40+
* should revert the pattern change and mark the trip as CANCELED on the scheduled pattern.
41+
*/
42+
@Test
43+
void cancelScheduledTripAfterSkippedStop() {
44+
var env = ENV_BUILDER.addTrip(TRIP_INPUT).build();
45+
var rt = GtfsRtTestHelper.of(env);
46+
47+
// Step 1: Skip stop B — creates a modified pattern
48+
var skipUpdate = rt
49+
.tripUpdateScheduled(TRIP_1_ID)
50+
.addDelayedStopTime(0, 0)
51+
.addSkippedStop(1)
52+
.addDelayedStopTime(2, 90)
53+
.build();
54+
55+
assertSuccess(rt.applyTripUpdate(skipUpdate, DIFFERENTIAL));
56+
57+
// Verify the modified pattern was created
58+
var snapshot = env.timetableSnapshot();
59+
assertNotNull(
60+
snapshot.getNewTripPatternForModifiedTrip(id(TRIP_1_ID), env.defaultServiceDate()),
61+
"A modified trip pattern should exist after skipping a stop"
62+
);
63+
64+
assertEquals(
65+
"UPDATED | A 0:01 0:01:01 | B [C] 0:01:52 0:01:58 | C 0:02:50 0:02:51",
66+
env.tripData(TRIP_1_ID).showTimetable()
67+
);
68+
69+
// Step 2: Cancel the trip
70+
var cancelUpdate = rt.tripUpdate(TRIP_1_ID, CANCELED).build();
71+
assertSuccess(rt.applyTripUpdate(cancelUpdate, DIFFERENTIAL));
72+
73+
// Verify the modified pattern is cleaned up
74+
snapshot = env.timetableSnapshot();
75+
assertNull(
76+
snapshot.getNewTripPatternForModifiedTrip(id(TRIP_1_ID), env.defaultServiceDate()),
77+
"Modified trip pattern should be removed after cancellation"
78+
);
79+
80+
// Trip should be CANCELED on the scheduled pattern
81+
var tripData = env.tripData(TRIP_1_ID);
82+
assertEquals(RealTimeState.CANCELED, tripData.realTimeState());
83+
assertTrue(tripData.tripTimes().isCanceledOrDeleted());
84+
85+
assertEquals(
86+
"CANCELED | A 0:01 0:01:01 | B 0:01:10 0:01:11 | C 0:01:20 0:01:21",
87+
tripData.showTimetable()
88+
);
89+
}
90+
}

application/src/test/java/org/opentripplanner/updater/trip/siri/moduletests/extracall/ExtraCallTest.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.opentripplanner.updater.trip.siri.moduletests.extracall;
22

3+
import static com.google.common.truth.Truth.assertThat;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure;
56
import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess;
@@ -50,6 +51,10 @@ void testExtraCall() {
5051
);
5152
}
5253

54+
/**
55+
* Apply the same extra call update twice (identical message). Verifies idempotency: the trip
56+
* times and the MODIFIED pattern are unchanged after the second application.
57+
*/
5358
@Test
5459
void testExtraCallMultipleTimes() {
5560
var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).build();
@@ -91,6 +96,101 @@ void testExtraCallAndCancellation() {
9196
);
9297
}
9398

99+
/**
100+
* Add an extra call (A → D(extra) → B), then send a second update with the same extra call
101+
* but different times. Unlike {@link #testExtraCallMultipleTimes()} which replays an identical
102+
* message, this test verifies that updated times are actually applied while preserving the
103+
* extra call and the MODIFIED pattern.
104+
*/
105+
@Test
106+
void testExtraCallThenUpdateTimesKeepsExtraCall() {
107+
var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).build();
108+
var siri = SiriTestHelper.of(env);
109+
110+
// Step 1: Add extra call D between A and B
111+
var extraCallUpdate = updateWithExtraCall(siri);
112+
assertSuccess(siri.applyEstimatedTimetable(extraCallUpdate));
113+
114+
assertEquals(
115+
"MODIFIED | A [R] 0:00:15 0:00:15 | D [EC] 0:00:20 0:00:25 | B 0:00:33 0:00:33",
116+
env.tripData(TRIP_1_ID).showTimetable()
117+
);
118+
assertThat(env.raptorData().summarizePatterns()).containsExactly(
119+
"F:route-id::001:RT[MODIFIED]"
120+
);
121+
122+
// Step 2: Send update with same extra call but different times
123+
var updatedTimes = siri
124+
.etBuilder()
125+
.withDatedVehicleJourneyRef(TRIP_1_ID)
126+
.withLineRef(ROUTE_ID)
127+
.withRecordedCalls(builder -> builder.call(STOP_A).departAimedActual("00:00:11", "00:00:16"))
128+
.withEstimatedCalls(builder ->
129+
builder
130+
.call(STOP_D)
131+
.withIsExtraCall(true)
132+
.arriveAimedExpected("00:00:18", "00:00:22")
133+
.departAimedExpected("00:00:19", "00:00:27")
134+
.call(STOP_B)
135+
.arriveAimedExpected("00:00:20", "00:00:35")
136+
)
137+
.buildEstimatedTimetableDeliveries();
138+
139+
var result = siri.applyEstimatedTimetable(updatedTimes);
140+
assertSuccess(result);
141+
142+
// Extra call D should still be present with updated times
143+
assertEquals(
144+
"MODIFIED | A [R] 0:00:16 0:00:16 | D [EC] 0:00:22 0:00:27 | B 0:00:35 0:00:35",
145+
env.tripData(TRIP_1_ID).showTimetable()
146+
);
147+
var patterns = env.raptorData().summarizePatterns();
148+
assertThat(patterns).hasSize(1);
149+
assertThat(patterns.stream().findFirst().get()).endsWith("[MODIFIED]");
150+
}
151+
152+
/**
153+
* Add an extra call (A → D(extra) → B), then send a regular update without the extra call
154+
* (A → B with updated times). The trip should revert to the scheduled pattern.
155+
*/
156+
@Test
157+
void testExtraCallThenRevertToOriginalStops() {
158+
var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).build();
159+
var siri = SiriTestHelper.of(env);
160+
161+
// Step 1: Add extra call D between A and B
162+
var extraCallUpdate = updateWithExtraCall(siri);
163+
assertSuccess(siri.applyEstimatedTimetable(extraCallUpdate));
164+
165+
assertEquals(
166+
"MODIFIED | A [R] 0:00:15 0:00:15 | D [EC] 0:00:20 0:00:25 | B 0:00:33 0:00:33",
167+
env.tripData(TRIP_1_ID).showTimetable()
168+
);
169+
assertThat(env.raptorData().summarizePatterns()).containsExactly(
170+
"F:route-id::001:RT[MODIFIED]"
171+
);
172+
173+
// Step 2: Send regular update without extra call — just A → B with updated times
174+
var revert = siri
175+
.etBuilder()
176+
.withDatedVehicleJourneyRef(TRIP_1_ID)
177+
.withRecordedCalls(builder -> builder.call(STOP_A).departAimedActual("00:00:11", "00:00:16"))
178+
.withEstimatedCalls(builder ->
179+
builder.call(STOP_B).arriveAimedExpected("00:00:20", "00:00:30")
180+
)
181+
.buildEstimatedTimetableDeliveries();
182+
183+
var result = siri.applyEstimatedTimetable(revert);
184+
assertSuccess(result);
185+
186+
// Trip should revert to the scheduled pattern with UPDATED state
187+
assertEquals(
188+
"UPDATED | A [R] 0:00:16 0:00:16 | B 0:00:30 0:00:30",
189+
env.tripData(TRIP_1_ID).showTimetable()
190+
);
191+
assertThat(env.raptorData().summarizePatterns()).containsExactly("F:Pattern1[UPDATED]");
192+
}
193+
94194
@Test
95195
void testExtraUnknownStop() {
96196
var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).build();

0 commit comments

Comments
 (0)