Skip to content

Commit

Permalink
Upgrade UniFFI; add more extensive core integration test for iOS; fir…
Browse files Browse the repository at this point in the history
…st stage tests for Android
  • Loading branch information
ianthetechie committed Nov 15, 2023
1 parent 3e5ea05 commit 09a2e27
Show file tree
Hide file tree
Showing 14 changed files with 563 additions and 347 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
"revision" : "6199f3f502ad926653a9d5d1e12206f52f0aa55e"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "4862d48562483d274a2ac7522d905c9237a31a48",
"version" : "1.15.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
Expand Down
16 changes: 11 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ if useLocalFramework {
path: "./common/target/ios/libferrostar-rs.xcframework"
)
} else {
let releaseTag = "0.0.12"
let releaseChecksum = "11a0edc5f3a8c912091cdd7cecc867dfba0cad1867d97e3c6227349fe903ccc8"
let releaseTag = "0.0.13"
let releaseChecksum = "041951f5c8aaf44bd60d5a90861d24cba4821a4601d1b091a9b7a64d739e01c4"
binaryTarget = .binaryTarget(
name: "FerrostarCoreRS",
url: "https://github.com/stadiamaps/ferrostar/releases/download/\(releaseTag)/libferrostar-rs.xcframework.zip",
Expand All @@ -28,7 +28,7 @@ if useLocalFramework {
let package = Package(
name: "FerrostarCore",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
Expand All @@ -42,8 +42,11 @@ let package = Package(
),
],
dependencies: [
// .package(url: "https://github.com/maplibre/maplibre-gl-native-distribution", .upToNextMajor(from: "5.13.0")),
.package(url: "https://github.com/stadiamaps/maplibre-swiftui-dsl-playground", branch: "main"),
.package(
url: "https://github.com/pointfreeco/swift-snapshot-testing",
from: "1.15.0"
),
],
targets: [
binaryTarget,
Expand All @@ -69,7 +72,10 @@ let package = Package(
),
.testTarget(
name: "FerrostarCoreTests",
dependencies: ["FerrostarCore"],
dependencies: [
"FerrostarCore",
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
],
path: "apple/Tests/FerrostarCoreTests"
),
]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ and it leverages macros.

### iOS

We plan to start iOS support at version 16.
We plan to start iOS support at version 15.
Our general policy will be to support the current and at least the previous major version,
extending to two major versions if possible.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.stadiamaps.ferrostar.core

import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.mock.MediaTypes
import okhttp3.mock.MockInterceptor
import okhttp3.mock.eq
import okhttp3.mock.get
import okhttp3.mock.post
import okhttp3.mock.respond
import okhttp3.mock.rule
import okhttp3.mock.url
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Test
import uniffi.ferrostar.GeographicCoordinates
import uniffi.ferrostar.Route
import uniffi.ferrostar.RouteAdapter
import uniffi.ferrostar.RouteRequest
import uniffi.ferrostar.RouteRequestGenerator
import uniffi.ferrostar.RouteResponseParser
import uniffi.ferrostar.UserLocation
import java.time.Instant

private val valhallaEndpointUrl = "https://api.stadiamaps.com/navigate/v1"

// Simple test to ensure that the extensibility with native code is working.

class MockRouteRequestGenerator: RouteRequestGenerator {
override fun generateRequest(
userLocation: UserLocation,
waypoints: List<GeographicCoordinates>
): RouteRequest = RouteRequest.HttpPost(valhallaEndpointUrl, mapOf(), byteArrayOf())

}

class MockRouteResponseParser(private val routes: List<Route>) : RouteResponseParser {
override fun parseResponse(response: ByteArray): List<Route> = routes
}

class FerrostarCoreTest {
private val errorBody = """
{
"error": "No valid authentication provided."
}
""".trimIndent().toResponseBody(MediaTypes.MEDIATYPE_JSON)

@Test
fun test401UnauthorizedRouteResponse() = runTest {
val interceptor = MockInterceptor().apply {
rule(post, url eq valhallaEndpointUrl) {
respond(401, errorBody)
}

rule(get) {
respond {
throw IllegalStateException("an IO error")
}
}
}

val core = FerrostarCore(
routeAdapter = RouteAdapter(requestGenerator = MockRouteRequestGenerator(), responseParser = MockRouteResponseParser(routes = listOf())),
locationProvider = SimulatedLocationProvider(),
httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build()
)

try {
// Tests that the core generates a request and attempts to process it, but throws due to the mocked network layer
core.getRoutes(
initialLocation = UserLocation(coordinates = GeographicCoordinates(-149.543469, 60.5347155), 0.0, null, Instant.now()),
waypoints = listOf(GeographicCoordinates(-149.5485806, 60.5349908))
)
fail("Expected the request to fail")
} catch (e: InvalidStatusCodeException) {
assertEquals(401, e.statusCode)
}
}

// TODO: successful test
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ const val simpleRoute = """
"""

class ValhallaCoreTest {
private val valhallaEndpointUrl = "https://api.stadiamaps.com/navigate"
private val valhallaEndpointUrl = "https://api.stadiamaps.com/navigate/v1"

@Test
fun parseValhallaRouteResponse(): TestResult {
Expand Down Expand Up @@ -295,4 +295,4 @@ class ValhallaCoreTest {
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,25 @@ import uniffi.ferrostar.NavigationController
import uniffi.ferrostar.NavigationControllerConfig
import uniffi.ferrostar.Route
import uniffi.ferrostar.RouteAdapter
import uniffi.ferrostar.RouteAdapterInterface
import uniffi.ferrostar.RouteRequest
import uniffi.ferrostar.StepAdvanceMode
import uniffi.ferrostar.UserLocation
import java.net.URL
import java.util.concurrent.Executor
import java.util.concurrent.Executors

class FerrostarCoreException : Exception {
open class FerrostarCoreException : Exception {
constructor(message: String) : super(message)
constructor(message: String, cause: Throwable) : super(message, cause)
constructor(cause: Throwable) : super(cause)
}

class InvalidStatusCodeException(val statusCode: Int): FerrostarCoreException("Route request failed with status code $statusCode")

class NoResponseBodyException: FerrostarCoreException("Route request was successful but had no body bytes")

public class FerrostarCore(
val routeAdapter: RouteAdapter,
val routeAdapter: RouteAdapterInterface,
val locationProvider: LocationProvider,
val httpClient: OkHttpClient
) : LocationUpdateListener {
Expand Down Expand Up @@ -59,9 +63,9 @@ public class FerrostarCore(
val res = httpClient.newCall(httpRequest).await()
val bodyBytes = res.body?.bytes()
if (!res.isSuccessful) {
throw FerrostarCoreException("Route request failed with status code ${res.code}")
throw InvalidStatusCodeException(res.code)
} else if (bodyBytes == null) {
throw FerrostarCoreException("Route request was successful but had no body bytes")
throw NoResponseBodyException()
}

return routeAdapter.parseResponse(bodyBytes)
Expand Down
30 changes: 25 additions & 5 deletions apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ import CoreLocation
@testable import FerrostarCore
import UniFFI
import XCTest
import SnapshotTesting

private let backendUrl = URL(string: "https://api.stadiamaps.com/route/v1")!
let errorBody = Data("""
{
"error": "No valid authentication provided."
}
""".utf8)
let errorResponse = HTTPURLResponse(url: backendUrl, statusCode: 401, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"])!
let errorResponse = HTTPURLResponse(url: valhallaEndpointUrl, statusCode: 401, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"])!

// Simple test to ensure that the extensibility with native code is working.

// NOTE: you can also implement RouteAdapterProtocol directly.

private class MockRouteRequestGenerator: RouteRequestGenerator {
func generateRequest(userLocation: UniFFI.UserLocation, waypoints: [UniFFI.GeographicCoordinates]) throws -> UniFFI.RouteRequest {
return UniFFI.RouteRequest.httpPost(url: backendUrl.absoluteString, headers: [:], body: Data())
return UniFFI.RouteRequest.httpPost(url: valhallaEndpointUrl.absoluteString, headers: [:], body: Data())
}
}

Expand All @@ -35,19 +36,38 @@ private class MockRouteResponseParser: RouteResponseParser {
final class FerrostarCoreTests: XCTestCase {
func test401UnauthorizedRouteResponse() async throws {
let mockSession = MockURLSession()
mockSession.registerMock(forURL: backendUrl, withData: errorBody, andResponse: errorResponse)
mockSession.registerMock(forURL: valhallaEndpointUrl, withData: errorBody, andResponse: errorResponse)

let routeAdapter = RouteAdapter(requestGenerator: MockRouteRequestGenerator(), responseParser: MockRouteResponseParser(routes: []))

let core = FerrostarCore(routeAdapter: routeAdapter, locationManager: SimulatedLocationProvider(), networkSession: mockSession)

do {
// Tests that the core generates a request and attempts to process it, but throws due to the mocked network layer
_ = try await core.getRoutes(initialLocation: CLLocation(latitude: 60.5347155, longitude: -149.543469), waypoints: [CLLocationCoordinate2D(latitude: 60.5349908, longitude: -149.5485806)])
XCTFail("Expected an error")
} catch let FerrostarCoreError.httpStatusCode(statusCode) {
XCTAssertEqual(statusCode, 401)
}
}

@MainActor
func test200MockRouteResponse() async throws {
let mockSession = MockURLSession()
mockSession.registerMock(forURL: valhallaEndpointUrl, withData: Data(), andResponse: successfulJSONResponse)

let geom = [GeographicCoordinates(lng: 0, lat: 0), GeographicCoordinates(lng: 1, lat: 1)]
let instructionContent = VisualInstructionContent(text: "Sail straight", maneuverType: .depart, maneuverModifier: .straight, roundaboutExitDegrees: nil)
let mockRoute = UniFFI.Route(geometry: geom, distance: 1, waypoints: geom, steps: [RouteStep(geometry: geom, distance: 1, roadName: "foo road", instruction: "Sail straight", visualInstructions: [VisualInstructions(primaryContent: instructionContent, secondaryContent: nil, triggerDistanceBeforeManeuver: 42)])])

let routeAdapter = RouteAdapter(requestGenerator: MockRouteRequestGenerator(), responseParser: MockRouteResponseParser(routes: [mockRoute]))

let core = FerrostarCore(routeAdapter: routeAdapter, locationManager: SimulatedLocationProvider(), networkSession: mockSession)

// Tests that the core generates a request and then the mocked parser returns the expected routes
let routes = try await core.getRoutes(initialLocation: CLLocation(latitude: 60.5347155, longitude: -149.543469), waypoints: [CLLocationCoordinate2D(latitude: 60.5349908, longitude: -149.5485806)])
assertSnapshot(of: routes, as: .dump)
}

// TODO: Various location services failure modes (need special mocks to simulate these)
}
Loading

0 comments on commit 09a2e27

Please sign in to comment.