Skip to content

Commit 7063f19

Browse files
AhmedVargosgithub-actions[bot]
authored andcommitted
NAVAND-6315 Adding instrumentation tests for happy routing scenarios for Walking and Cycling profiles (#10545)
* NAVAND-6315 Add new testing routes JSON meant for Walking and Cycling profiles * NAVAND-6315 Create a new CyclingAndWalkingRoutingTest class that cover basic happy scenario routing and rerouting for Cycling and Walking profiles * NAVAND-6315 Adding suggested improvements GitOrigin-RevId: 5183d38e25e51eaf5d63868734a59d72026d493a
1 parent 073eae5 commit 7063f19

File tree

7 files changed

+11811
-52
lines changed

7 files changed

+11811
-52
lines changed
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
package com.mapbox.navigation.instrumentation_tests.core
2+
3+
import android.location.Location
4+
import androidx.test.espresso.Espresso
5+
import com.adevinta.android.barista.rule.cleardata.ClearFilesRule
6+
import com.mapbox.api.directions.v5.DirectionsCriteria.PROFILE_CYCLING
7+
import com.mapbox.api.directions.v5.DirectionsCriteria.PROFILE_WALKING
8+
import com.mapbox.api.directions.v5.models.RouteOptions
9+
import com.mapbox.geojson.Point
10+
import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI
11+
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
12+
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
13+
import com.mapbox.navigation.base.trip.model.RouteProgressState
14+
import com.mapbox.navigation.core.directions.session.RoutesExtra.ROUTES_UPDATE_REASON_REROUTE
15+
import com.mapbox.navigation.core.internal.extensions.flowLocationMatcherResult
16+
import com.mapbox.navigation.instrumentation_tests.R
17+
import com.mapbox.navigation.testing.ui.BaseCoreNoCleanUpTest
18+
import com.mapbox.navigation.testing.ui.utils.MapboxNavigationRule
19+
import com.mapbox.navigation.testing.ui.utils.coroutines.getSuccessfulResultOrThrowException
20+
import com.mapbox.navigation.testing.ui.utils.coroutines.navigateNextRouteLeg
21+
import com.mapbox.navigation.testing.ui.utils.coroutines.requestRoutes
22+
import com.mapbox.navigation.testing.ui.utils.coroutines.routeProgressUpdates
23+
import com.mapbox.navigation.testing.ui.utils.coroutines.routesUpdates
24+
import com.mapbox.navigation.testing.ui.utils.coroutines.sdkTest
25+
import com.mapbox.navigation.testing.ui.utils.coroutines.setNavigationRoutesAsync
26+
import com.mapbox.navigation.testing.utils.assertions.assertSuccessfulRerouteStateTransition
27+
import com.mapbox.navigation.testing.utils.assertions.recordRerouteStates
28+
import com.mapbox.navigation.testing.utils.history.MapboxHistoryTestRule
29+
import com.mapbox.navigation.testing.utils.http.MockDirectionsRequestHandler
30+
import com.mapbox.navigation.testing.utils.location.MockLocationReplayerRule
31+
import com.mapbox.navigation.testing.utils.location.moveAlongTheRouteUntilTracking
32+
import com.mapbox.navigation.testing.utils.location.stayOnPosition
33+
import com.mapbox.navigation.testing.utils.readRawFileText
34+
import com.mapbox.navigation.testing.utils.routes.MockRoute
35+
import com.mapbox.navigation.testing.utils.routes.RoutesProvider
36+
import com.mapbox.navigation.testing.utils.withMapboxNavigation
37+
import com.mapbox.navigation.testing.utils.withoutInternet
38+
import com.mapbox.navigation.utils.internal.toPoint
39+
import com.mapbox.turf.TurfMeasurement
40+
import kotlinx.coroutines.flow.filter
41+
import kotlinx.coroutines.flow.first
42+
import org.junit.Assert.assertEquals
43+
import org.junit.Assume.assumeTrue
44+
import org.junit.Before
45+
import org.junit.Rule
46+
import org.junit.Test
47+
import org.junit.runner.RunWith
48+
import org.junit.runners.Parameterized
49+
50+
@OptIn(ExperimentalMapboxNavigationAPI::class)
51+
@RunWith(Parameterized::class)
52+
class CyclingAndWalkingRoutingTest(private val directionsProfile: String) :
53+
BaseCoreNoCleanUpTest() {
54+
55+
companion object {
56+
@JvmStatic
57+
@Parameterized.Parameters(name = "Profile={0}")
58+
fun provideDirectionProfiles(): Collection<String> = listOf(
59+
PROFILE_CYCLING,
60+
PROFILE_WALKING,
61+
)
62+
}
63+
64+
@get:Rule
65+
val mapboxNavigationRule = MapboxNavigationRule()
66+
67+
@get:Rule
68+
val mockLocationReplayerRule = MockLocationReplayerRule(mockLocationUpdatesRule)
69+
70+
@get:Rule
71+
val mapboxHistoryTestRule = MapboxHistoryTestRule()
72+
73+
/**
74+
* Files cleanup is required because tests that use [withoutInternet] in this class
75+
* expect navigator to not have enough tiles to build an offline route and fail
76+
* building a route.
77+
*/
78+
@get:Rule
79+
val clearFilesRule = ClearFilesRule()
80+
81+
@Before
82+
fun setup() {
83+
Espresso.onIdle()
84+
}
85+
86+
private val testInitialLocation by lazy {
87+
RoutesProvider.cycling_route_dc_very_short(context)
88+
.routeWaypoints.first()
89+
}
90+
91+
override fun setupMockLocation(): Location {
92+
val initialLocation = testInitialLocation
93+
return mockLocationUpdatesRule.generateLocationUpdate {
94+
latitude = initialLocation.latitude()
95+
longitude = initialLocation.longitude()
96+
}
97+
}
98+
99+
@Test
100+
fun reroute_completes() = sdkTest {
101+
val mockRoute = retrieveMatchingRouteFor(
102+
directionsProfile,
103+
cyclingMockRoute = RoutesProvider.cycling_route_dc_very_short(context),
104+
walkingMockRoute = RoutesProvider.walking_route_dc_very_short(context),
105+
)
106+
val originLocation = mockRoute.routeWaypoints.first()
107+
val offRouteLocationUpdate = mockLocationUpdatesRule.generateLocationUpdate {
108+
latitude = originLocation.latitude() + 0.002
109+
longitude = originLocation.longitude()
110+
}
111+
mockWebServerRule.requestHandlers.addAll(mockRoute.mockRequestHandlers)
112+
mockWebServerRule.requestHandlers.add(
113+
MockDirectionsRequestHandler(
114+
profile = directionsProfile,
115+
jsonResponse = retrieveMatchingRouteJsonFor(
116+
directionsProfile,
117+
cyclingRouteJson = R.raw.cycling_route_response_dc_very_short,
118+
walkingRouteJson = R.raw.walking_route_response_dc_very_short,
119+
),
120+
expectedCoordinates = listOf(
121+
Point.fromLngLat(
122+
offRouteLocationUpdate.longitude,
123+
offRouteLocationUpdate.latitude,
124+
),
125+
mockRoute.routeWaypoints.last(),
126+
),
127+
relaxedExpectedCoordinates = true,
128+
),
129+
)
130+
131+
withMapboxNavigation(
132+
historyRecorderRule = mapboxHistoryTestRule,
133+
) { navigation ->
134+
val rerouteStates = navigation.recordRerouteStates()
135+
val routes = stayOnPosition(originLocation, bearing = 0.0f) {
136+
navigation.startTripSession()
137+
navigation.requestRoutes(
138+
RouteOptions.builder()
139+
.applyDefaultNavigationOptions(directionsProfile)
140+
.applyLanguageAndVoiceUnitOptions(context)
141+
.baseUrl(mockWebServerRule.baseUrl)
142+
.coordinatesList(mockRoute.routeWaypoints)
143+
.build(),
144+
).getSuccessfulResultOrThrowException().routes
145+
}
146+
navigation.setNavigationRoutesAsync(routes)
147+
navigation.moveAlongTheRouteUntilTracking(
148+
routes[0],
149+
mockLocationReplayerRule,
150+
3,
151+
)
152+
stayOnPosition(offRouteLocationUpdate) {
153+
val routesUpdate = navigation.routesUpdates().first {
154+
it.reason == ROUTES_UPDATE_REASON_REROUTE
155+
}
156+
assertSuccessfulRerouteStateTransition(rerouteStates)
157+
val newWaypoints = routesUpdate.navigationRoutes.first()
158+
.directionsRoute.routeOptions()!!.coordinatesList()
159+
assertEquals(2, newWaypoints.size)
160+
assertEquals(mockRoute.routeWaypoints.last(), newWaypoints[1])
161+
navigation.routeProgressUpdates().first {
162+
it.currentState == RouteProgressState.TRACKING
163+
}
164+
}
165+
}
166+
}
167+
168+
@Test
169+
fun reroute_on_multileg_route_without_alternatives() = sdkTest {
170+
withMapboxNavigation(
171+
historyRecorderRule = mapboxHistoryTestRule,
172+
) { mapboxNavigation ->
173+
val mockRoute = retrieveMatchingRouteFor(
174+
directionsProfile,
175+
cyclingMockRoute = RoutesProvider.cycling_dc_very_short_two_legs(context),
176+
walkingMockRoute = RoutesProvider.walking_route_dc_very_short_two_legs(context),
177+
)
178+
val originalLocation = mockLocationUpdatesRule.generateLocationUpdate {
179+
latitude = mockRoute.routeWaypoints.first().latitude()
180+
longitude = mockRoute.routeWaypoints.first().longitude()
181+
}
182+
val secondLegLocation = mockLocationUpdatesRule.generateLocationUpdate {
183+
latitude = mockRoute.routeWaypoints[1].latitude()
184+
longitude = mockRoute.routeWaypoints[1].longitude()
185+
}
186+
val offRouteLocationUpdate = mockLocationUpdatesRule.generateLocationUpdate {
187+
latitude = secondLegLocation.latitude + 0.002
188+
longitude = secondLegLocation.longitude
189+
}
190+
191+
mockWebServerRule.requestHandlers.addAll(mockRoute.mockRequestHandlers)
192+
mockWebServerRule.requestHandlers.add(
193+
MockDirectionsRequestHandler(
194+
profile = directionsProfile,
195+
jsonResponse = retrieveMatchingRouteJsonFor(
196+
profile = directionsProfile,
197+
cyclingRouteJson = R.raw.cycling_route_response_dc_very_short_two_legs,
198+
walkingRouteJson = R.raw.walking_route_response_dc_very_short_two_legs,
199+
),
200+
expectedCoordinates = listOf(
201+
Point.fromLngLat(
202+
offRouteLocationUpdate.longitude,
203+
offRouteLocationUpdate.latitude,
204+
),
205+
mockRoute.routeWaypoints.last(),
206+
),
207+
relaxedExpectedCoordinates = true,
208+
),
209+
)
210+
val rerouteStates = mapboxNavigation.recordRerouteStates()
211+
212+
mapboxNavigation.startTripSession()
213+
val routes = mapboxNavigation.requestRoutes(
214+
RouteOptions.builder()
215+
.applyDefaultNavigationOptions(directionsProfile)
216+
.applyLanguageAndVoiceUnitOptions(context)
217+
.baseUrl(mockWebServerRule.baseUrl)
218+
.coordinatesList(mockRoute.routeWaypoints).build(),
219+
).getSuccessfulResultOrThrowException().routes
220+
mapboxNavigation.setNavigationRoutes(routes)
221+
222+
mapboxNavigation.moveAlongTheRouteUntilTracking(
223+
routes[0],
224+
mockLocationReplayerRule,
225+
)
226+
mockLocationReplayerRule.loopUpdateUntil(originalLocation) {
227+
mapboxNavigation.routeProgressUpdates().first()
228+
}
229+
mapboxNavigation.routeProgressUpdates().first()
230+
mapboxNavigation.navigateNextRouteLeg()
231+
mockLocationReplayerRule.loopUpdateUntil(secondLegLocation) {
232+
mapboxNavigation.routeProgressUpdates()
233+
.filter { it.currentLegProgress?.legIndex == 1 }
234+
.first()
235+
}
236+
mockLocationReplayerRule.loopUpdateUntil(offRouteLocationUpdate) {
237+
mapboxNavigation.routeProgressUpdates()
238+
.filter { it.currentState == RouteProgressState.OFF_ROUTE }
239+
.first()
240+
}
241+
242+
mapboxNavigation.routesUpdates().filter {
243+
(it.reason == ROUTES_UPDATE_REASON_REROUTE).also { didUpdate ->
244+
if (didUpdate) {
245+
assertEquals(0, mapboxNavigation.currentLegIndex())
246+
}
247+
}
248+
}.first()
249+
assertSuccessfulRerouteStateTransition(rerouteStates)
250+
}
251+
}
252+
253+
@Test
254+
fun reroute_on_multileg_route_first_leg_with_alternatives() = sdkTest(timeout = 40_000) {
255+
withMapboxNavigation(historyRecorderRule = mapboxHistoryTestRule) { mapboxNavigation ->
256+
// Skip this test for walking profile, as it doesn't support alternatives.
257+
assumeTrue(directionsProfile != PROFILE_WALKING)
258+
259+
val rerouteStates = mapboxNavigation.recordRerouteStates()
260+
val mockRoute = RoutesProvider.cycling_dc_short_two_legs_with_alternative(context)
261+
mockWebServerRule.requestHandlers.addAll(mockRoute.mockRequestHandlers)
262+
val routes = mapboxNavigation.requestRoutes(
263+
RouteOptions.builder()
264+
.applyDefaultNavigationOptions(directionsProfile)
265+
.applyLanguageAndVoiceUnitOptions(context)
266+
.baseUrl(mockWebServerRule.baseUrl)
267+
.coordinatesList(mockRoute.routeWaypoints).build(),
268+
).getSuccessfulResultOrThrowException().routes
269+
270+
mockLocationReplayerRule.playRoute(routes[1].directionsRoute)
271+
mapboxNavigation.startTripSession()
272+
// make sure new initial location is overridden by new location updates
273+
mapboxNavigation.flowLocationMatcherResult().first {
274+
TurfMeasurement.distance(
275+
it.enhancedLocation.toPoint(),
276+
testInitialLocation,
277+
) > 0.1
278+
}
279+
mapboxNavigation.setNavigationRoutesAsync(routes)
280+
281+
val rerouteResult = mapboxNavigation.routesUpdates().filter {
282+
(it.reason == ROUTES_UPDATE_REASON_REROUTE).also { didUpdate ->
283+
if (didUpdate) {
284+
assertEquals(0, mapboxNavigation.currentLegIndex())
285+
}
286+
}
287+
}.first()
288+
assertEquals(routes[1], rerouteResult.navigationRoutes.first())
289+
assertSuccessfulRerouteStateTransition(rerouteStates)
290+
}
291+
}
292+
293+
private fun retrieveMatchingRouteJsonFor(
294+
profile: String,
295+
cyclingRouteJson: Int,
296+
walkingRouteJson: Int,
297+
): String =
298+
when (profile) {
299+
PROFILE_CYCLING -> {
300+
readRawFileText(
301+
context,
302+
cyclingRouteJson,
303+
)
304+
}
305+
306+
PROFILE_WALKING -> {
307+
readRawFileText(
308+
context,
309+
walkingRouteJson,
310+
)
311+
}
312+
313+
else -> {
314+
readRawFileText(context, walkingRouteJson)
315+
}
316+
}
317+
318+
private fun retrieveMatchingRouteFor(
319+
profile: String,
320+
cyclingMockRoute: MockRoute,
321+
walkingMockRoute: MockRoute,
322+
): MockRoute =
323+
when (profile) {
324+
PROFILE_CYCLING -> cyclingMockRoute
325+
PROFILE_WALKING -> walkingMockRoute
326+
else -> cyclingMockRoute
327+
}
328+
}

0 commit comments

Comments
 (0)