diff --git a/CHANGELOG.md b/CHANGELOG.md index 740815520a6..4be01dab317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changes to the Mapbox Navigation SDK for iOS +## v2.20.0 + +### Routing + +* Move alternative route parsing to the background thread. ([#4729](https://github.com/mapbox/mapbox-navigation-ios/pull/4729)) + ## v2.19.0 ### Packaging @@ -12,6 +18,7 @@ * Added handling `RouteResponse.refreshTTL` into account when refreshing a route. Now it will no longer be possible to attmept to refresh and outdated route, and `Router` will inform that current route has expired using `RouterDelegate.routerDidFailToRefreshExpiredRoute(:_)` method. ([#4672](https://github.com/mapbox/mapbox-navigation-ios/pull/4672)) ### Other changes + * Fixed next banner view correctly appearing when steps list view is expanded. ([#4708](https://github.com/mapbox/mapbox-navigation-ios/pull/4708)) * Fixed rare route simulation issue where user's speed was calculated and NaN and the puck did not move. ([#4708](https://github.com/mapbox/mapbox-navigation-ios/pull/4708)) * Fixed a possibly not-updating `StepsViewController` after reroutes when using a custom top bar. ([#4716](https://github.com/mapbox/mapbox-navigation-ios/pull/4716)) diff --git a/Sources/MapboxCoreNavigation/AlternativeRoute.swift b/Sources/MapboxCoreNavigation/AlternativeRoute.swift index 3a82ac9ec9d..265a8847de6 100644 --- a/Sources/MapboxCoreNavigation/AlternativeRoute.swift +++ b/Sources/MapboxCoreNavigation/AlternativeRoute.swift @@ -43,17 +43,22 @@ public struct AlternativeRoute: Identifiable { /// The difference of expected travel time between alternative and the main routes public let expectedTravelTimeDelta: TimeInterval - init?(mainRoute: Route, alternativeRoute nativeRouteAlternative: RouteAlternative) { - self.id = nativeRouteAlternative.id - guard let decoded = RerouteController.decode(routeRequest: nativeRouteAlternative.route.getRequestUri(), - routeResponse: nativeRouteAlternative.route.getResponseJsonRef()) else { + init?(alternativeRoute nativeRouteAlternative: RouteAlternative) { + guard let indexedRouteResponse = nativeRouteAlternative.indexedRouteResponse() else { return nil } + self.init(indexedRouteResponse: indexedRouteResponse, alternativeRoute: nativeRouteAlternative) + } + + init?(indexedRouteResponse: IndexedRouteResponse, alternativeRoute nativeRouteAlternative: RouteAlternative) { + var indexedRouteResponse = indexedRouteResponse + indexedRouteResponse.routeIndex = Int(nativeRouteAlternative.route.getRouteIndex()) + guard let mainRoute = indexedRouteResponse.currentRoute else { + return nil + } + self.id = nativeRouteAlternative.id + self.indexedRouteResponse = indexedRouteResponse - self.indexedRouteResponse = .init(routeResponse: decoded.routeResponse, - routeIndex: Int(nativeRouteAlternative.route.getRouteIndex()), - responseOrigin: nativeRouteAlternative.route.getRouterOrigin()) - var legIndex = Int(nativeRouteAlternative.mainRouteFork.legIndex) var segmentIndex = Int(nativeRouteAlternative.mainRouteFork.segmentIndex) @@ -122,3 +127,19 @@ extension Route { return leg.steps[stepindex].intersections?[intersectionIndex] } } + +extension RouteAlternative { + func indexedRouteResponse() -> IndexedRouteResponse? { + guard let decoded = + RerouteController.decode(routeRequest: route.getRequestUri(), + routeResponse: route.getResponseJsonRef()) else { + return nil + } + + return IndexedRouteResponse( + routeResponse: decoded.routeResponse, + routeIndex: Int(route.getRouteIndex()), + responseOrigin: route.getRouterOrigin() + ) + } +} diff --git a/Sources/MapboxCoreNavigation/RerouteController.swift b/Sources/MapboxCoreNavigation/RerouteController.swift index eda32a66cef..d08e10393fa 100644 --- a/Sources/MapboxCoreNavigation/RerouteController.swift +++ b/Sources/MapboxCoreNavigation/RerouteController.swift @@ -70,7 +70,10 @@ class RerouteController { } // MARK: Internal State Management - + + private static let defaultParsingQueueLabel = "com.mapbox.navigation.reroute.parsing" + private let parsingQueue: DispatchQueue + private let defaultRerouteController: DefaultRerouteControllerInterface private let rerouteDetector: RerouteDetectorInterface @@ -92,12 +95,17 @@ class RerouteController { } } - required init(_ navigator: MapboxNavigationNative.Navigator, config: ConfigHandle) { + init( + _ navigator: MapboxNavigationNative.Navigator, + config: ConfigHandle, + parsingQueue: DispatchQueue = .init(label: RerouteController.defaultParsingQueueLabel, qos: .default) + ) { self.navigator = navigator self.config = config self.defaultRerouteController = DefaultRerouteControllerInterface(nativeInterface: navigator.getRerouteController()) self.navigator?.setRerouteControllerForController(defaultRerouteController) self.rerouteDetector = navigator.getRerouteDetector() + self.parsingQueue = parsingQueue self.navigator?.addRerouteObserver(for: self) } @@ -121,16 +129,18 @@ class RerouteController { extension RerouteController: RerouteObserver { func onSwitchToAlternative(forRoute route: RouteInterface) { - guard let decoded = Self.decode(routeRequest: route.getRequestUri(), - routeResponse: route.getResponseJsonRef()) else { - return + parsingQueue.async { [weak self] in + guard let self else { return } + guard let decoded = Self.decode(routeRequest: route.getRequestUri(), + routeResponse: route.getResponseJsonRef()) else { + return + } + self.delegate?.rerouteControllerWantsSwitchToAlternative(self, + response: decoded.routeResponse, + routeIndex: Int(route.getRouteIndex()), + options: decoded.routeOptions, + routeOrigin: route.getRouterOrigin()) } - - delegate?.rerouteControllerWantsSwitchToAlternative(self, - response: decoded.routeResponse, - routeIndex: Int(route.getRouteIndex()), - options: decoded.routeOptions, - routeOrigin: route.getRouterOrigin()) } func onRerouteDetected(forRouteRequest routeRequest: String) -> Bool { @@ -156,18 +166,20 @@ extension RerouteController: RerouteObserver { routeOrigin: origin) self.latestRouteResponse = nil } else { - guard let responseData = routeResponse.data(using: .utf8), - let decodedResponse = Self.decode(routeResponse: responseData, - routeOptions: decodedRequest.routeOptions, - credentials: decodedRequest.credentials) else { - delegate?.rerouteControllerDidFailToReroute(self, with: DirectionsError.invalidResponse(nil)) - return + parsingQueue.async { [weak self] in + guard let self else { return } + guard let responseData = routeResponse.data(using: .utf8), + let decodedResponse = Self.decode(routeResponse: responseData, + routeOptions: decodedRequest.routeOptions, + credentials: decodedRequest.credentials) else { + self.delegate?.rerouteControllerDidFailToReroute(self, with: DirectionsError.invalidResponse(nil)) + return + } + self.delegate?.rerouteControllerDidRecieveReroute(self, + response: decodedResponse, + options: decodedRequest.routeOptions, + routeOrigin: origin) } - - delegate?.rerouteControllerDidRecieveReroute(self, - response: decodedResponse, - options: decodedRequest.routeOptions, - routeOrigin: origin) } } diff --git a/Sources/MapboxCoreNavigation/RouteController.swift b/Sources/MapboxCoreNavigation/RouteController.swift index e2c4f2ef425..70bb4e63d18 100644 --- a/Sources/MapboxCoreNavigation/RouteController.swift +++ b/Sources/MapboxCoreNavigation/RouteController.swift @@ -33,6 +33,7 @@ open class RouteController: NSObject { /// Holds currently alive instance of `RouteController`. private static weak var instance: RouteController? private static let instanceLock: NSLock = .init() + private static let defaultParsingQueueLabel = "com.mapbox.navigation.route.parsing" let sessionUUID: UUID = .init() private var isInitialized: Bool = false @@ -224,7 +225,10 @@ open class RouteController: NSObject { var routeTask: NavigationProviderRequest? public private(set) var continuousAlternatives: [AlternativeRoute] = [] - + + private let parsingQueue: DispatchQueue + private let delegateQueue: DispatchQueue + /** Enables automatic switching to online version of the current route when possible. @@ -285,9 +289,8 @@ open class RouteController: NSObject { private func updateNavigator(with indexedRouteResponse: IndexedRouteResponse, fromLegIndex legIndex: Int, reason: RouteChangeReason, - completion: ((Result<(RouteInfo?, [AlternativeRoute]), Error>) -> Void)?) { - guard let newMainRoute = indexedRouteResponse.currentRoute, - let routesData = indexedRouteResponse.routesData(routeParserType: routeParserType) else { + completion: ((Result) -> Void)?) { + guard let routesData = indexedRouteResponse.routesData(routeParserType: routeParserType) else { completion?(.failure(RouteControllerError.failedToSerializeRoute)) return } @@ -295,30 +298,38 @@ open class RouteController: NSObject { uuid: sessionUUID, legIndex: UInt32(legIndex), reason: reason, - completion: { [weak self] result in + completion: { [weak self] result in guard let self = self else { return } switch result { case .success(let info): - let alternativeRoutes = info.1.compactMap { - AlternativeRoute(mainRoute: newMainRoute, - alternativeRoute: $0) - } let alerts = routesData.primaryRoute().getRouteInfo().alerts - self.routeAlerts = alerts.reduce(into: [:]) { dictionary, alert in + let routeAlerts = alerts.reduce(into: [:]) { dictionary, alert in dictionary[alert.roadObject.id] = alert } - completion?(.success((info.0, alternativeRoutes))) - - let removedRoutes = self.continuousAlternatives.filter { alternative in - !alternativeRoutes.contains(where: { - alternative.id == $0.id - }) + self.routeAlerts = routeAlerts + completion?(.success(())) + + self.parsingQueue.async { [weak self] in + guard let self else { return } + let alternativeRoutes = info.1.compactMap { + AlternativeRoute(alternativeRoute: $0) + } + + self.delegateQueue.async { [weak self] in + guard let self else { return } + + let removedRoutes = self.continuousAlternatives.filter { alternative in + !alternativeRoutes.contains(where: { + alternative.id == $0.id + }) + } + self.continuousAlternatives = alternativeRoutes + self.report(newAlternativeRoutes: alternativeRoutes, + removedAlternativeRoutes: removedRoutes) + } } - self.continuousAlternatives = alternativeRoutes - self.report(newAlternativeRoutes: alternativeRoutes, - removedAlternativeRoutes: removedRoutes) - + case .failure(let error): completion?(.failure(error)) } @@ -677,6 +688,8 @@ open class RouteController: NSObject { self.routeProgress = RouteProgress(route: indexedRouteResponse.currentRoute!, options: options) self.refreshesRoute = isRouteOptions && options.profileIdentifier == .automobileAvoidingTraffic && options.refreshingEnabled + self.parsingQueue = DispatchQueue(label: RouteController.defaultParsingQueueLabel, qos: .default) + self.delegateQueue = .main super.init() @@ -690,7 +703,9 @@ open class RouteController: NSObject { dataSource source: RouterDataSource, navigatorType: CoreNavigator.Type, routeParserType: RouteParser.Type, - navigationSessionManager: NavigationSessionManager) { + navigationSessionManager: NavigationSessionManager, + parsingQueue: DispatchQueue = .init(label: RouteController.defaultParsingQueueLabel, qos: .default), + delegateQueue: DispatchQueue = .main) { Self.checkUniqueInstance() self.navigatorType = navigatorType @@ -700,6 +715,8 @@ open class RouteController: NSObject { let options = indexedRouteResponse.validatedRouteOptions self.routeProgress = RouteProgress(route: indexedRouteResponse.currentRoute!, options: options) self.navigationSessionManager = navigationSessionManager + self.parsingQueue = parsingQueue + self.delegateQueue = delegateQueue super.init() @@ -1095,22 +1112,28 @@ extension RouteController { switch result { case .success(let routeAlternatives): - let alternativeRoutes = routeAlternatives.compactMap { - AlternativeRoute(mainRoute: self.route, - alternativeRoute: $0) - } - - removedRoutes.append(contentsOf: alternatives.filter { alternative in - !routeAlternatives.contains(where: { - alternative.id == $0.id + self.parsingQueue.async { [weak self] in + guard let self else { return } + + let alternativeRoutes = routeAlternatives.compactMap { + AlternativeRoute(alternativeRoute: $0) + } + + removedRoutes.append(contentsOf: alternatives.filter { alternative in + !routeAlternatives.contains(where: { + alternative.id == $0.id + }) + }.compactMap { + AlternativeRoute(alternativeRoute: $0) }) - }.compactMap { - AlternativeRoute(mainRoute: self.route, - alternativeRoute: $0) - }) - self.continuousAlternatives = alternativeRoutes - self.report(newAlternativeRoutes: alternativeRoutes, - removedAlternativeRoutes: removedRoutes) + + self.delegateQueue.async { [weak self] in + guard let self else { return } + self.continuousAlternatives = alternativeRoutes + self.report(newAlternativeRoutes: alternativeRoutes, + removedAlternativeRoutes: removedRoutes) + } + } case .failure(let updateError): let error = AlternativeRouteError.failedToUpdateAlternativeRoutes(reason: updateError.localizedDescription) var userInfo = [RouteController.NotificationUserInfoKey: Any]() @@ -1127,11 +1150,11 @@ extension RouteController { guard NavigationSettings.shared.alternativeRouteDetectionStrategy != nil else { return } - + var userInfo = [RouteController.NotificationUserInfoKey: Any]() userInfo[.updatedAlternativesKey] = newAlternativeRoutes userInfo[.removedAlternativesKey] = removedAlternativeRoutes - + NotificationCenter.default.post(name: .routeControllerDidUpdateAlternatives, object: self, userInfo: userInfo) diff --git a/Sources/MapboxCoreNavigation/Router.swift b/Sources/MapboxCoreNavigation/Router.swift index fba6e858499..c67b0dc73ab 100644 --- a/Sources/MapboxCoreNavigation/Router.swift +++ b/Sources/MapboxCoreNavigation/Router.swift @@ -45,21 +45,24 @@ public struct IndexedRouteResponse { - returns: Array of `AlternativeRoutes` containing relative information to `currentRoute`. */ public func parseAlternativeRoutes() -> [AlternativeRoute] { - guard let mainRoute = currentRoute, - let routesData = routesData(routeParserType: RouteParser.self) else { + guard let routesData = routesData(routeParserType: RouteParser.self) else { return [] } let alternatives = routesData.alternativeRoutes().compactMap { - AlternativeRoute(mainRoute: mainRoute, - alternativeRoute: $0) + AlternativeRoute(indexedRouteResponse: self, alternativeRoute: $0) } return alternatives } - - func routesData(routeParserType: RouteParser.Type) -> RoutesData? { - let routeOptions = validatedRouteOptions - + + private let nativeRoutes: [RouteInterface]? + + static func routeInterfaces( + routeParserType: RouteParser.Type, + routeResponse: RouteResponse, + routeOptions: RouteOptions, + responseOrigin: RouterOrigin + ) -> [RouteInterface]? { let encoder = JSONEncoder() encoder.userInfo[.options] = routeOptions guard let routeData = try? encoder.encode(routeResponse) else { @@ -73,14 +76,22 @@ public struct IndexedRouteResponse { request: routeRequest, routeOrigin: responseOrigin) if parsedRoutes.isValue(), - var routes = parsedRoutes.value as? [RouteInterface], - routes.indices.contains(routeIndex) { - return routeParserType.createRoutesData(forPrimaryRoute: routes.remove(at: routeIndex), - alternativeRoutes: routes) + let routes = parsedRoutes.value as? [RouteInterface] { + return routes } return nil } - + + func routesData(routeParserType: RouteParser.Type) -> RoutesData? { + guard var nativeRoutes, + nativeRoutes.indices.contains(routeIndex) else { + return nil + } + + return routeParserType.createRoutesData(forPrimaryRoute: nativeRoutes.remove(at: routeIndex), + alternativeRoutes: nativeRoutes) + } + /** Initializes a new `IndexedRouteResponse` object. @@ -102,14 +113,27 @@ public struct IndexedRouteResponse { init(routeResponse: RouteResponse, routeIndex: Int, - responseOrigin: RouterOrigin) { + responseOrigin: RouterOrigin, + routeParserType: RouteParser.Type = RouteParser.self + ) { self.routeResponse = routeResponse self.routeIndex = routeIndex self.responseOrigin = responseOrigin + self.nativeRoutes = Self.routeInterfaces( + routeParserType: routeParserType, + routeResponse: routeResponse, + routeOptions: routeResponse.validatedOptions, + responseOrigin: responseOrigin) } internal var validatedRouteOptions: RouteOptions { - switch routeResponse.options { + return routeResponse.validatedOptions + } +} + +extension RouteResponse { + var validatedOptions: RouteOptions { + switch options { case let .match(matchOptions): return RouteOptions(matchOptions: matchOptions) case let .route(options): diff --git a/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift b/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift index fb83720fda3..af09138438c 100644 --- a/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift +++ b/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift @@ -38,7 +38,7 @@ class NavigationServiceTests: TestCase { let expectationsTimeout = 1.0 - let indexedRouteResponse = IndexedRouteResponse.init(routeResponse: Fixture.routeResponse(from: jsonFileName, options: routeOptions), routeIndex: 0) + var indexedRouteResponse: IndexedRouteResponse! var location: CLLocation! var lastLocation: CLLocation! var userInfo: [String: String?] = ["key": "value"] @@ -73,6 +73,7 @@ class NavigationServiceTests: TestCase { override func setUp() { super.setUp() + indexedRouteResponse = IndexedRouteResponse(routeResponse: Fixture.routeResponse(from: jsonFileName, options: routeOptions), routeIndex: 0) delegate = NavigationServiceDelegateSpy() locationManager = NavigationLocationManagerSpy() customRoutingProvider = RoutingProviderSpy() diff --git a/Tests/MapboxCoreNavigationTests/RerouteControllerTests.swift b/Tests/MapboxCoreNavigationTests/RerouteControllerTests.swift index 32102bd72e1..3d1e480c2d5 100644 --- a/Tests/MapboxCoreNavigationTests/RerouteControllerTests.swift +++ b/Tests/MapboxCoreNavigationTests/RerouteControllerTests.swift @@ -71,7 +71,10 @@ final class RerouteControllerTests: TestCase { customRoutingProvider = .init() delegate = .init() - rerouteController = .init(navigatorSpy, config: configHandle) + rerouteController = RerouteController( + navigatorSpy, + config: configHandle, + parsingQueue: .main) rerouteController.delegate = delegate } diff --git a/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift b/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift index c4a3ec5ba64..58b75f687c7 100644 --- a/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift +++ b/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift @@ -87,6 +87,7 @@ class RouteControllerTests: TestCase { routeController = makeRouteController() routeController.delegate = delegate + waitUntilInitialAlternativesApplied() } override func tearDown() { @@ -233,8 +234,20 @@ class RouteControllerTests: TestCase { } func testFallbackToOffline() { + let alternativesExpectation = expectation(description: "Alternative route should be reported") + delegate.onDidUpdateAlternativeRoutes = { _,_ in + alternativesExpectation.fulfill() + } RouteParserSpy.returnedRoutes = [nativeRoute] + routeController.indexedRouteResponse = IndexedRouteResponse( + routeResponse: routeResponse, + routeIndex: 0, + responseOrigin: .online, + routeParserType: RouteParserSpy.self) NotificationCenter.default.post(name: .navigationDidSwitchToFallbackVersion, object: nil, userInfo: nil) + + waitForExpectations(timeout: expectationsTimeout) + XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertTrue(navigatorSpy.passedRoute === nativeRoute) XCTAssertEqual(navigatorSpy.passedUuid, routeController.sessionUUID) @@ -243,8 +256,19 @@ class RouteControllerTests: TestCase { } func testRestoreToOnline() { + let alternativesExpectation = expectation(description: "Alternative route should be reported") + delegate.onDidUpdateAlternativeRoutes = { _,_ in + alternativesExpectation.fulfill() + } RouteParserSpy.returnedRoutes = [nativeRoute] + routeController.indexedRouteResponse = IndexedRouteResponse( + routeResponse: routeResponse, + routeIndex: 0, + responseOrigin: .online, + routeParserType: RouteParserSpy.self) NotificationCenter.default.post(name: .navigationDidSwitchToTargetVersion, object: nil, userInfo: nil) + waitForExpectations(timeout: expectationsTimeout) + XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertTrue(navigatorSpy.passedRoute === nativeRoute) XCTAssertEqual(navigatorSpy.passedUuid, routeController.sessionUUID) @@ -453,6 +477,7 @@ class RouteControllerTests: TestCase { func testUpdateIndexesWhenNavigationStatusDidChange() { let response = IndexedRouteResponse(routeResponse: multilegRouteResponse, routeIndex: 0) routeController = makeRouteController(routeResponse: response) + waitUntilInitialAlternativesApplied() routeController.locationManager(locationManagerSpy, didUpdateLocations: [rawLocation]) let activeGuidanceInfo = makeActiveGuidanceInfo() @@ -636,6 +661,8 @@ class RouteControllerTests: TestCase { routingProvider.returnedRoutesResult = .success(response) routeController.reroute(from: rawLocation, along: routeProgress) + waitForExpectations(timeout: expectationsTimeout) + XCTAssertFalse(routeController.isRerouting) XCTAssertFalse(rerouteController.forceRerouteCalled) XCTAssertTrue(routingProvider.calculateRoutesCalled) @@ -643,8 +670,6 @@ class RouteControllerTests: TestCase { let expectedRouteOptions = routeProgress.reroutingOptions(from: rawLocation) XCTAssertEqual(routingProvider.passedRouteOptions, expectedRouteOptions) - - waitForExpectations(timeout: expectationsTimeout) } func testSendRerouteNotificationIfRoutIsEmptyRoute() { @@ -676,21 +701,30 @@ class RouteControllerTests: TestCase { routingProvider.returnedRoutesResult = .success(response) routeController.reroute(from: rawLocation, along: routeProgress) + waitForExpectations(timeout: expectationsTimeout) + XCTAssertFalse(routeController.isRerouting) XCTAssertFalse(rerouteController.forceRerouteCalled) XCTAssertTrue(routingProvider.calculateRoutesCalled) let expectedRouteOptions = routeProgress.reroutingOptions(from: rawLocation) XCTAssertEqual(routingProvider.passedRouteOptions, expectedRouteOptions) - - waitForExpectations(timeout: expectationsTimeout) } func testRerouteWhenReroutingAndNavigatorSucceed() { let response = IndexedRouteResponse(routeResponse: routeResponse, routeIndex: 0) routingProvider.returnedRoutesResult = .success(response) + + let expectation = expectation(description: "Did reroute call on delegate") + delegate.onDidRerouteAlong = { parameters in + XCTAssertTrue(parameters.route === response.currentRoute) + XCTAssertFalse(parameters.proactive) + expectation.fulfill() + } routeController.reroute(from: rawLocation, along: routeProgress) + waitForExpectations(timeout: expectationsTimeout) + XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertEqual(navigatorSpy.passedUuid, routeController.sessionUUID) XCTAssertEqual(navigatorSpy.passedLegIndex, 0) @@ -710,11 +744,15 @@ class RouteControllerTests: TestCase { return } navigationSessionManagerSpy.reset() + + let completionExpectation = expectation(description: "completion") routeController.updateRoute(with: response, routeOptions: routeOptions, isProactive: false, - isAlternative: false, - completion: nil) + isAlternative: false) { _ in + completionExpectation.fulfill() + } + waitForExpectations(timeout: expectationsTimeout) XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertEqual(navigatorSpy.passedUuid, routeController.sessionUUID) @@ -733,11 +771,15 @@ class RouteControllerTests: TestCase { func testUpdateRouteIfShouldNotStartNewBillingSession() { let response = IndexedRouteResponse(routeResponse: singleRouteResponse, routeIndex: 0) routingProvider.returnedRoutesResult = .success(response) + let completionExpectation = expectation(description: "completion") routeController.updateRoute(with: response, routeOptions: options, isProactive: false, - isAlternative: false, - completion: nil) + isAlternative: false) { _ in + completionExpectation.fulfill() + } + + waitForExpectations(timeout: expectationsTimeout) XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertEqual(navigatorSpy.passedUuid, routeController.sessionUUID) @@ -778,8 +820,14 @@ class RouteControllerTests: TestCase { let response = IndexedRouteResponse(routeResponse: singleRouteResponse, routeIndex: 0) routingProvider.returnedRoutesResult = .success(response) navigatorSpy.returnedSetRoutesResult = .failure(DirectionsError.unableToRoute) - + + let callbackExpectation = expectation(description: "Did fail to reroute call on delegate") + delegate.onDidFailToRerouteWith = { _ in + callbackExpectation.fulfill() + } routeController.reroute(from: rawLocation, along: routeProgress) + waitForExpectations(timeout: expectationsTimeout) + XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertFalse(routeController.isRerouting) } @@ -820,10 +868,17 @@ class RouteControllerTests: TestCase { _ = routeController.rerouteControllerDidDetectReroute(rerouteController) let routerOrigin = indexedRouteResponse.responseOrigin + let callbackExpectation = expectation(description: "Did reroute call on delegate") + delegate.onDidRerouteAlong = { arguments in + XCTAssertTrue(arguments.route === response.currentRoute) + callbackExpectation.fulfill() + } routeController.rerouteControllerDidRecieveReroute(rerouteController, response: response.routeResponse, options: options, routeOrigin: routerOrigin) + waitForExpectations(timeout: expectationsTimeout) + XCTAssertFalse(routeController.isRerouting) XCTAssertTrue(routeController.routeProgress.route === response.currentRoute) } @@ -947,11 +1002,10 @@ class RouteControllerTests: TestCase { updatedRouteProgress.currentLegProgress = legProgress routeController.lastProactiveRerouteDate = locationWithDate.timestamp.addingTimeInterval(-121) routeController.checkForFasterRoute(from: locationWithDate, routeProgress: updatedRouteProgress) - + waitForExpectations(timeout: expectationsTimeout) + XCTAssertNil(routeController.lastProactiveRerouteDate) XCTAssertFalse(routeController.isRerouting) - - waitForExpectations(timeout: expectationsTimeout) } func testProactiveReroutingIfNotFasterRoad() { @@ -970,10 +1024,9 @@ class RouteControllerTests: TestCase { updatedRouteProgress.currentLegProgress = legProgress routeController.lastProactiveRerouteDate = locationWithDate.timestamp.addingTimeInterval(-121) routeController.checkForFasterRoute(from: locationWithDate, routeProgress: updatedRouteProgress) - - XCTAssertNil(routeController.lastProactiveRerouteDate) - waitForExpectations(timeout: expectationsTimeout) + + XCTAssertNil(routeController.lastProactiveRerouteDate) } func testProactiveReroutingIfNonNilCompletion() { @@ -985,20 +1038,24 @@ class RouteControllerTests: TestCase { routeExpectation.fulfill() return true } - + let rerouteExpectation = expectation(description: "Did reroute call on delegate") + delegate.onDidRerouteAlong = { _ in + rerouteExpectation.fulfill() + } + let updatedRouteProgress = self.routeProgress! updatedRouteProgress.currentLegProgress.currentStep.expectedTravelTime = 2000 updatedRouteProgress.currentLegProgress.currentStepProgress.distanceTraveled = 2 routeController.lastProactiveRerouteDate = locationWithDate.timestamp.addingTimeInterval(-121) routeController.checkForFasterRoute(from: locationWithDate, routeProgress: updatedRouteProgress) - + + waitForExpectations(timeout: expectationsTimeout) + XCTAssertNil(routeController.lastProactiveRerouteDate) XCTAssertFalse(routeController.isRerouting) XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertTrue(routeController.didProactiveReroute) - - waitForExpectations(timeout: expectationsTimeout) } func testProactiveReroutingIfNoCompletion() { @@ -1083,11 +1140,11 @@ class RouteControllerTests: TestCase { ] navigatorSpy.returnedSetAlternativeRoutesResult = .success([alternativeRoute]) NotificationCenter.default.post(name: .navigatorDidChangeAlternativeRoutes, object: nil, userInfo: userInfo) + waitForExpectations(timeout: expectationsTimeout) XCTAssertTrue(navigatorSpy.setAlternativeRoutesCalled) XCTAssertEqual(routeController.continuousAlternatives.count, 1) XCTAssertEqual(routeController.continuousAlternatives[0].id, 1) - waitForExpectations(timeout: expectationsTimeout) } func testSendAlternativeRoutesNotificationIfEmptyCurrentContinuousAlternatives() { @@ -1117,6 +1174,7 @@ class RouteControllerTests: TestCase { func testAlternativeRoutesReportedIfNonEmptyCurrentContinuousAlternatives() { navigatorSpy.returnedSetRoutesResult = .success((mainRouteInfo: nil, alternativeRoutes: [createRouteAlternative(id: 2)])) routeController.updateRoute(with: indexedRouteResponse, routeOptions: options, completion: nil) + waitUntilInitialAlternativesApplied() let alternativesExpectation = expectation(description: "Alternative route should be reported") delegate.onDidUpdateAlternativeRoutes = { newAlternatives, removedAlternatives in @@ -1134,15 +1192,16 @@ class RouteControllerTests: TestCase { navigatorSpy.returnedSetAlternativeRoutesResult = .success([alternativeRoute]) NotificationCenter.default.post(name: .navigatorDidChangeAlternativeRoutes, object: nil, userInfo: userInfo) + waitForExpectations(timeout: expectationsTimeout) XCTAssertTrue(navigatorSpy.setAlternativeRoutesCalled) XCTAssertEqual(routeController.continuousAlternatives.count, 1) XCTAssertEqual(routeController.continuousAlternatives[0].id, 1) - waitForExpectations(timeout: expectationsTimeout) } func testSendAlternativeRoutesNotificationIfNonEmptyCurrentContinuousAlternatives() { navigatorSpy.returnedSetRoutesResult = .success((mainRouteInfo: nil, alternativeRoutes: [createRouteAlternative(id: 2)])) routeController.updateRoute(with: indexedRouteResponse, routeOptions: options, completion: nil) + waitUntilInitialAlternativesApplied() expectation(forNotification: .routeControllerDidUpdateAlternatives, object: routeController) { (notification) -> Bool in let userInfo = notification.userInfo @@ -1281,11 +1340,11 @@ class RouteControllerTests: TestCase { let userInfo = [Navigator.NotificationUserInfoKey.coincideOnlineRouteKey: route] NotificationCenter.default.post(name: .navigatorWantsSwitchToCoincideOnlineRoute, object: nil, userInfo: userInfo) + waitForExpectations(timeout: expectationsTimeout) XCTAssertEqual(routeController.indexedRouteResponse.routeIndex, 0) XCTAssertEqual(routeController.indexedRouteResponse.responseOrigin, route.getRouterOrigin()) XCTAssertEqual(routeController.indexedRouteResponse.currentRoute?.legs, singleDecodedRoute.legs) - waitForExpectations(timeout: expectationsTimeout) } func testRerouteAfterArrivalIfUserHasNotArrivedAtWaypoint() { @@ -1381,10 +1440,10 @@ class RouteControllerTests: TestCase { let status = TestNavigationStatusProvider.createNavigationStatus(routeState: .complete) routeController.rerouteAfterArrivalIfNeeded(rawLocation, status: status) + waitForExpectations(timeout: expectationsTimeout) XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertEqual(routeController.indexedRouteResponse.currentRoute, newResponse.currentRoute) - waitForExpectations(timeout: expectationsTimeout) } func testDoNotUpdateRouteIfFinishedRouting() { @@ -1410,8 +1469,9 @@ class RouteControllerTests: TestCase { XCTAssertFalse(result) completionExpectation.fulfill() } - XCTAssertEqual(routeController.route, route) waitForExpectations(timeout: expectationsTimeout) + + XCTAssertEqual(routeController.route, route) } func testUpdateRouteIfIfNavNativeSucceed() { @@ -1422,53 +1482,72 @@ class RouteControllerTests: TestCase { XCTAssertTrue(result) completionExpectation.fulfill() } + waitForExpectations(timeout: expectationsTimeout) + XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertEqual(routeController.route, routeResponse.routes?[1]) - waitForExpectations(timeout: expectationsTimeout) } func testUpdateRouteIfRouteParserFailed() { RouteParserSpy.returnedError = "error" - let newResponse = IndexedRouteResponse(routeResponse: routeResponse, routeIndex: 1) + let newResponse = IndexedRouteResponse( + routeResponse: routeResponse, + routeIndex: 1, + responseOrigin: .online, + routeParserType: RouteParserSpy.self + ) let completionExpectation = expectation(description: "Should call callback") routeController.updateRoute(with: newResponse, routeOptions: options) { result in XCTAssertFalse(result) completionExpectation.fulfill() } - XCTAssertFalse(navigatorSpy.setRoutesCalled) - XCTAssertEqual(routeController.route, routeResponse.routes?[0], "Should not change rout") waitForExpectations(timeout: expectationsTimeout) + + XCTAssertFalse(navigatorSpy.setRoutesCalled) + XCTAssertEqual(routeController.route, routeResponse.routes?[0], "Should not change route") } func testUpdateRouteIfRouteParserSucceedButNotEnoughRoutes() { RouteParserSpy.returnedRoutes = [nativeRoute] - let newResponse = IndexedRouteResponse(routeResponse: routeResponse, routeIndex: 1) + let newResponse = IndexedRouteResponse( + routeResponse: routeResponse, + routeIndex: 1, + responseOrigin: .online, + routeParserType: RouteParserSpy.self + ) let completionExpectation = expectation(description: "Should call callback") routeController.updateRoute(with: newResponse, routeOptions: options) { result in XCTAssertFalse(result) completionExpectation.fulfill() } - XCTAssertFalse(navigatorSpy.setRoutesCalled) waitForExpectations(timeout: expectationsTimeout) + + XCTAssertFalse(navigatorSpy.setRoutesCalled) } func testUpdateRouteIfRouteParserSucceed() { RouteParserSpy.returnedRoutes = [nativeRoute, nativeRoute] - let newResponse = IndexedRouteResponse(routeResponse: routeResponse, routeIndex: 1) + let newResponse = IndexedRouteResponse( + routeResponse: routeResponse, + routeIndex: 1, + responseOrigin: .online, + routeParserType: RouteParserSpy.self + ) let completionExpectation = expectation(description: "Should call callback") routeController.updateRoute(with: newResponse, routeOptions: options) { result in XCTAssertTrue(result) completionExpectation.fulfill() } + waitForExpectations(timeout: expectationsTimeout) + XCTAssertTrue(navigatorSpy.setRoutesCalled) XCTAssertTrue(navigatorSpy.passedRoute === nativeRoute) XCTAssertEqual(navigatorSpy.passedUuid, routeController.sessionUUID) XCTAssertEqual(navigatorSpy.passedLegIndex, UInt32(routeController.routeProgress.legIndex)) XCTAssertEqual(navigatorSpy.passedReason, .reroute) - waitForExpectations(timeout: expectationsTimeout) } func testHandleWillSwitchToAlternativeEvent() { @@ -1588,8 +1667,9 @@ class RouteControllerTests: TestCase { routeController.updateSpokenInstructionProgress(status: status, willReRoute: false) - XCTAssertFalse(routeController.didProactiveReroute) waitForExpectations(timeout: expectationsTimeout) + + XCTAssertFalse(routeController.didProactiveReroute) } func testDoNotUpdateVisualInstructionProgressIfNilBannerInstruction() { @@ -1703,12 +1783,23 @@ class RouteControllerTests: TestCase { dataSource: dataSource, navigatorType: CoreNavigatorSpy.self, routeParserType: RouteParserSpy.self, - navigationSessionManager: navigationSessionManagerSpy) + navigationSessionManager: navigationSessionManagerSpy, + parsingQueue: .main, + delegateQueue: .main) controller.delegate = delegate navigatorSpy.reset() return controller } + private func waitUntilInitialAlternativesApplied() { + let initializedExpectation = expectation(description: "initial alternatives set") + delegate.onDidUpdateAlternativeRoutes = { [weak self] _,_ in + initializedExpectation.fulfill() + self?.delegate.onDidUpdateAlternativeRoutes = nil + } + wait(for: [initializedExpectation], timeout: expectationsTimeout) + } + private func makeRouteResponse() -> RouteResponse { return Fixture.routeResponse(from: "routeResponseWithAlternatives", options: options) }