Skip to content

Commit a8e23f7

Browse files
committed
NAVAND-995: add NativeNavigatorTest
1 parent 28b7efc commit a8e23f7

File tree

6 files changed

+4478
-0
lines changed

6 files changed

+4478
-0
lines changed

.circleci/config.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,17 @@ commands:
202202
module_target:
203203
description: module target
204204
type: string
205+
inject_token:
206+
description: whether to inject an access token file
207+
type: boolean
208+
default: false
205209
steps:
206210
- run:
207211
name: Assemble Instrumentation Test APK for << parameters.module_target >>
208212
command: |
213+
if << parameters.inject_token >>; then
214+
echo "${MAPBOX_DEVELOPER_CONFIG}" > /root/code/<< parameters.module_target >>/src/androidTest/res/values/mapbox_access_token.xml
215+
fi
209216
./gradlew << parameters.module_target >>:assembleAndroidTest
210217
211218
login-google-cloud-platform:
@@ -532,6 +539,7 @@ jobs:
532539
variant: "Debug"
533540
- assemble-instrumentation-test:
534541
module_target: "libnavigation-core"
542+
inject_token: true
535543
- assemble-instrumentation-test:
536544
module_target: "libnavigator"
537545
- write-workspace

libnavigation-core/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
mobile-event-schemas.jsonl.gz
22
src/main/assets/sdk_versions/*
3+
src/androidTest/res/values/mapbox_access_token.xml
34

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package com.mapbox.navigation.core.trip.service
2+
3+
import android.location.Location
4+
import com.mapbox.api.directions.v5.DirectionsCriteria
5+
import com.mapbox.api.directions.v5.models.DirectionsResponse
6+
import com.mapbox.api.directions.v5.models.DirectionsRoute
7+
import com.mapbox.api.directions.v5.models.RouteOptions
8+
import com.mapbox.geojson.Point
9+
import com.mapbox.navigation.base.internal.route.nativeRoute
10+
import com.mapbox.navigation.base.options.NavigationOptions
11+
import com.mapbox.navigation.base.route.NavigationRoute
12+
import com.mapbox.navigation.base.route.RouterOrigin
13+
import com.mapbox.navigation.base.route.toNavigationRoute
14+
import com.mapbox.navigation.base.route.toNavigationRoutes
15+
import com.mapbox.navigation.core.MapboxNavigation
16+
import com.mapbox.navigation.core.MapboxNavigationProvider
17+
import com.mapbox.navigation.core.test.R
18+
import com.mapbox.navigation.core.tests.activity.TripServiceActivity
19+
import com.mapbox.navigation.navigator.internal.MapboxNativeNavigatorImpl
20+
import com.mapbox.navigation.testing.ui.BaseTest
21+
import com.mapbox.navigation.testing.ui.utils.runOnMainSync
22+
import com.mapbox.navigator.FixLocation
23+
import com.mapbox.navigator.NavigationStatusOrigin
24+
import com.mapbox.navigator.Navigator
25+
import com.mapbox.navigator.RouteInterface
26+
import com.mapbox.navigator.RouteParser
27+
import com.mapbox.navigator.SetRoutesDataParams
28+
import com.mapbox.navigator.SetRoutesParams
29+
import com.mapbox.navigator.SetRoutesReason
30+
import kotlinx.coroutines.Dispatchers
31+
import kotlinx.coroutines.delay
32+
import kotlinx.coroutines.runBlocking
33+
import kotlinx.coroutines.suspendCancellableCoroutine
34+
import kotlinx.coroutines.withTimeout
35+
import org.junit.After
36+
import org.junit.Assert.assertEquals
37+
import org.junit.Assert.assertTrue
38+
import org.junit.Before
39+
import org.junit.Ignore
40+
import org.junit.Test
41+
import java.util.Collections
42+
import java.util.Date
43+
import kotlin.coroutines.resume
44+
45+
internal class NativeNavigatorCallbackOrderTest :
46+
BaseTest<TripServiceActivity>(TripServiceActivity::class.java) {
47+
48+
private val startLocation = Point.fromLngLat(-121.496066, 38.577764)
49+
private lateinit var mapboxNavigation: MapboxNavigation
50+
private lateinit var navigator: Navigator
51+
private lateinit var routes: List<NavigationRoute>
52+
private lateinit var route: NavigationRoute
53+
private lateinit var multilegRoute: NavigationRoute
54+
private lateinit var refreshedRouteResponse: String
55+
private lateinit var callbackInvocations: MutableList<CallbackInvocation>
56+
57+
override fun setupMockLocation(): Location {
58+
return mockLocationUpdatesRule.generateLocationUpdate {
59+
latitude = startLocation.latitude()
60+
longitude = startLocation.longitude()
61+
}
62+
}
63+
64+
@Before
65+
fun setUp() {
66+
runOnMainSync {
67+
callbackInvocations = Collections.synchronizedList(mutableListOf<CallbackInvocation>())
68+
mapboxNavigation = MapboxNavigationProvider.create(
69+
NavigationOptions.Builder(activity)
70+
.accessToken(activity.getString(R.string.mapbox_access_token))
71+
.build()
72+
)
73+
// starts raw location updates - otherwise we don't get onStatus calls
74+
mapboxNavigation.startTripSession()
75+
val nativeNavigatorField = mapboxNavigation.javaClass.getDeclaredField("navigator")
76+
nativeNavigatorField.isAccessible = true
77+
val nativeNavigatorImpl =
78+
nativeNavigatorField.get(mapboxNavigation) as MapboxNativeNavigatorImpl
79+
nativeNavigatorField.isAccessible = false
80+
val navigatorField = nativeNavigatorImpl.javaClass.getDeclaredField("navigator")
81+
navigatorField.isAccessible = true
82+
navigator = navigatorField.get(nativeNavigatorImpl) as Navigator
83+
navigatorField.isAccessible = false
84+
routes = DirectionsResponse.fromJson(
85+
activity.resources.openRawResource(R.raw.route_response_route_refresh)
86+
.readBytes().decodeToString(),
87+
RouteOptions.builder()
88+
.profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC)
89+
.coordinatesList(
90+
listOf(
91+
startLocation,
92+
Point.fromLngLat(-121.480256, 38.576795)
93+
)
94+
)
95+
.build()
96+
).routes().toNavigationRoutes(RouterOrigin.Custom())
97+
route = routes.first()
98+
refreshedRouteResponse = activity.resources
99+
.openRawResource(R.raw.route_response_route_refresh_annotations)
100+
.readBytes().decodeToString()
101+
multilegRoute = DirectionsRoute.fromJson(
102+
activity.resources.openRawResource(R.raw.multileg_route)
103+
.readBytes().decodeToString()
104+
).toNavigationRoute(RouterOrigin.Custom())
105+
}
106+
}
107+
108+
@After
109+
fun tearDown() {
110+
MapboxNavigationProvider.destroy()
111+
}
112+
113+
@Test
114+
fun setRoutes() = runBlocking(Dispatchers.Main.immediate) {
115+
navigator.addObserver { origin, _ ->
116+
callbackInvocations.add(CallbackInvocation.Status(origin))
117+
}
118+
waitForStatusUpdatesToBegin()
119+
navigator.setRoutes(
120+
SetRoutesParams(route.nativeRoute(), 0, emptyList<RouteInterface>()),
121+
SetRoutesReason.NEW_ROUTE
122+
) { callbackInvocations.add(CallbackInvocation.RoutesSet) }
123+
callbackInvocations.waitUntilHas(CallbackInvocation.RoutesSet, elementsAfter = 1)
124+
125+
callbackInvocations.checkThatHas(
126+
CallbackInvocation.Status(NavigationStatusOrigin.SET_ROUTE)
127+
) strictlyAfter CallbackInvocation.RoutesSet
128+
}
129+
130+
@Test
131+
fun setRoutesData() = runBlocking(Dispatchers.Main.immediate) {
132+
navigator.addObserver { origin, _ ->
133+
callbackInvocations.add(CallbackInvocation.Status(origin))
134+
}
135+
waitForStatusUpdatesToBegin()
136+
navigator.setRoutesData(
137+
SetRoutesDataParams(
138+
RouteParser.createRoutesData(
139+
route.nativeRoute(),
140+
listOf(routes[1].nativeRoute())
141+
),
142+
0
143+
),
144+
SetRoutesReason.NEW_ROUTE
145+
) {
146+
callbackInvocations.add(CallbackInvocation.RoutesDataSet)
147+
}
148+
149+
callbackInvocations.waitUntilHas(CallbackInvocation.RoutesDataSet, elementsAfter = 1)
150+
151+
callbackInvocations.checkThatHas(
152+
CallbackInvocation.Status(NavigationStatusOrigin.SET_ROUTE)
153+
) strictlyAfter CallbackInvocation.RoutesDataSet
154+
}
155+
156+
@Test
157+
fun refreshRoute() = runBlocking(Dispatchers.Main.immediate) {
158+
navigator.addObserver { origin, _ ->
159+
callbackInvocations.add(CallbackInvocation.Status(origin))
160+
}
161+
navigator.setRoutesAndWaitForResult(
162+
SetRoutesParams(route.nativeRoute(), 0, emptyList<RouteInterface>()),
163+
SetRoutesReason.NEW_ROUTE,
164+
)
165+
// we will expect SET_ROUTE after refresh, clear the previous ones
166+
callbackInvocations.waitUntilHas(
167+
CallbackInvocation.Status(NavigationStatusOrigin.SET_ROUTE)
168+
)
169+
callbackInvocations.clear()
170+
171+
waitForStatusUpdatesToBegin()
172+
navigator.refreshRoute(refreshedRouteResponse) {
173+
callbackInvocations.add(CallbackInvocation.Refresh)
174+
}
175+
176+
callbackInvocations.waitUntilHas(CallbackInvocation.Refresh, elementsAfter = 1)
177+
178+
callbackInvocations.checkThatHas(
179+
CallbackInvocation.Status(NavigationStatusOrigin.SET_ROUTE)
180+
) strictlyAfter CallbackInvocation.Refresh
181+
}
182+
183+
@Test
184+
fun changeLeg() = runBlocking(Dispatchers.Main.immediate) {
185+
navigator.setRoutesAndWaitForResult(
186+
SetRoutesParams(multilegRoute.nativeRoute(), 0, emptyList<RouteInterface>()),
187+
SetRoutesReason.NEW_ROUTE,
188+
)
189+
navigator.addObserver { origin, _ ->
190+
callbackInvocations.add(CallbackInvocation.Status(origin))
191+
}
192+
waitForStatusUpdatesToBegin()
193+
194+
navigator.changeLeg(1) {
195+
callbackInvocations.add(CallbackInvocation.LegChanged)
196+
}
197+
callbackInvocations.waitUntilHas(CallbackInvocation.LegChanged, elementsAfter = 1)
198+
199+
callbackInvocations.checkThatHas(
200+
CallbackInvocation.Status(NavigationStatusOrigin.LEG_CHANGE)
201+
) strictlyAfter CallbackInvocation.LegChanged
202+
}
203+
204+
@Ignore("bump NN to 124.0.0 (includes NN-361)")
205+
@Test
206+
fun updateLocation() = runBlocking(Dispatchers.Main.immediate) {
207+
navigator.addObserver { origin, status ->
208+
callbackInvocations.add(CallbackInvocation.Status(origin, status.location))
209+
}
210+
waitForStatusUpdatesToBegin()
211+
212+
val location = getSecondLocation()
213+
navigator.updateLocation(location) {
214+
callbackInvocations.add(CallbackInvocation.LocationUpdated)
215+
}
216+
callbackInvocations.waitUntilHas(CallbackInvocation.LocationUpdated, elementsAfter = 1)
217+
218+
callbackInvocations.checkThatHas(
219+
CallbackInvocation.Status(NavigationStatusOrigin.LOCATION_UPDATE, location)
220+
) strictlyAfter CallbackInvocation.LocationUpdated
221+
}
222+
223+
private fun List<CallbackInvocation>.checkThatHas(
224+
hasWhat: CallbackInvocation
225+
): CallbackInvocationsCheckThatHasScope = CallbackInvocationsCheckThatHasScope(this, hasWhat)
226+
227+
private infix fun CallbackInvocationsCheckThatHasScope.strictlyAfter(
228+
milestone: CallbackInvocation
229+
) {
230+
val index = actual.indexOf(milestone)
231+
val beforeMilestone = actual.take(index)
232+
val afterMilestone = actual[index + 1]
233+
assertTrue(
234+
"Statuses before $milestone should not contain $hasWhat, " +
235+
"actual: $this",
236+
beforeMilestone.none { it == hasWhat }
237+
)
238+
assertEquals(hasWhat, afterMilestone)
239+
}
240+
241+
private fun getSecondLocation(): FixLocation {
242+
// second location on route
243+
val longitude = -121.496166
244+
val latitude = 38.577533
245+
return FixLocation(
246+
Point.fromLngLat(longitude, latitude),
247+
System.nanoTime(),
248+
Date(),
249+
5f,
250+
120f,
251+
12f,
252+
1f,
253+
"fused",
254+
1f,
255+
1f,
256+
1f,
257+
HashMap(),
258+
false
259+
)
260+
}
261+
262+
private suspend fun List<*>.waitUntilHasSize(size: Int) {
263+
waitUntilCondition { it.size >= size }
264+
}
265+
266+
private suspend fun List<CallbackInvocation>.waitUntilHas(
267+
hasWhat: CallbackInvocation,
268+
elementsAfter: Int = 0
269+
) {
270+
waitUntilCondition {
271+
val indexOf = it.indexOf(hasWhat)
272+
indexOf != -1 && indexOf + elementsAfter <= it.lastIndex
273+
}
274+
}
275+
276+
private suspend fun <T> List<T>.waitUntilCondition(condition: (List<T>) -> Boolean) {
277+
val list = this
278+
withTimeout(10000) {
279+
while (!condition(list)) {
280+
delay(50)
281+
}
282+
}
283+
}
284+
285+
private suspend fun Navigator.setRoutesAndWaitForResult(
286+
params: SetRoutesParams,
287+
reason: SetRoutesReason,
288+
) = suspendCancellableCoroutine<Unit> { cont ->
289+
setRoutes(
290+
params,
291+
reason
292+
) { cont.resume(Unit) }
293+
}
294+
295+
private suspend fun waitForStatusUpdatesToBegin() {
296+
// 2 is not important: may be 1, may be 3, may be 10
297+
callbackInvocations.waitUntilHasSize(2)
298+
}
299+
}
300+
301+
private data class CallbackInvocationsCheckThatHasScope(
302+
val actual: List<CallbackInvocation>,
303+
val hasWhat: CallbackInvocation
304+
)
305+
306+
private sealed class CallbackInvocation {
307+
308+
data class Status(
309+
val origin: NavigationStatusOrigin,
310+
val location: FixLocation? = null
311+
) : CallbackInvocation()
312+
313+
object RoutesSet : CallbackInvocation()
314+
object RoutesDataSet : CallbackInvocation()
315+
object Refresh : CallbackInvocation()
316+
object LegChanged : CallbackInvocation()
317+
object LocationUpdated : CallbackInvocation()
318+
}

libnavigation-core/src/androidTest/res/raw/multileg_route.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)