style(sidecar): make ktlint clean, formatting only

ktlint -F over every kotlin file except DriverBackend.kt, then hand
fixes where the reflow read worse and for the long lines ktlint cannot
break. No behaviour changes.

DriverBackend.kt is left untouched to avoid a conflict with concurrent
work; its three over-long lines still fail fmt-kotlin.
This commit is contained in:
pj committed 2026-08-15 20:34:21 +05:30
1 parent 11f72a722a
commit 4d8e790bf1
12 files changed
+483 -156

No files matched your search

@@ -31,7 +31,10 @@ class DriverService(
private val launchedBundleId = AtomicReference<String?>(null) private val launchedBundleId = AtomicReference<String?>(null)
private val snapshotLock = Any() private val snapshotLock = Any()
override fun launch(request: LaunchRequest, responseObserver: StreamObserver<Empty>) { override fun launch(
request: LaunchRequest,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
backend.launch(request.bundleId, request.clearState, request.envMap) backend.launch(request.bundleId, request.clearState, request.envMap)
launchedBundleId.set(request.bundleId) launchedBundleId.set(request.bundleId)
@@ -39,7 +42,10 @@ class DriverService(
} }
} }
override fun terminate(request: Empty, responseObserver: StreamObserver<Empty>) { override fun terminate(
request: Empty,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
launchedBundleId.get()?.let { backend.terminate(it) } launchedBundleId.get()?.let { backend.terminate(it) }
launchedBundleId.set(null) launchedBundleId.set(null)
@@ -54,42 +60,60 @@ class DriverService(
} }
} }
override fun doubleTap(request: Point, responseObserver: StreamObserver<Empty>) { override fun doubleTap(
request: Point,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
backend.doubleTap(request.x, request.y) backend.doubleTap(request.x, request.y)
Empty.getDefaultInstance() Empty.getDefaultInstance()
} }
} }
override fun longPress(request: Point, responseObserver: StreamObserver<Empty>) { override fun longPress(
request: Point,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
backend.longPress(request.x, request.y) backend.longPress(request.x, request.y)
Empty.getDefaultInstance() Empty.getDefaultInstance()
} }
} }
override fun tapSelector(request: Selector, responseObserver: StreamObserver<Empty>) { override fun tapSelector(
request: Selector,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
backend.tapSelector(request.value) backend.tapSelector(request.value)
Empty.getDefaultInstance() Empty.getDefaultInstance()
} }
} }
override fun inputText(request: Text, responseObserver: StreamObserver<Empty>) { override fun inputText(
request: Text,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
backend.inputText(request.value) backend.inputText(request.value)
Empty.getDefaultInstance() Empty.getDefaultInstance()
} }
} }
override fun eraseText(request: EraseTextRequest, responseObserver: StreamObserver<Empty>) { override fun eraseText(
request: EraseTextRequest,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
backend.eraseText(request.characterCount) backend.eraseText(request.characterCount)
Empty.getDefaultInstance() Empty.getDefaultInstance()
} }
} }
override fun swipe(request: SwipeRequest, responseObserver: StreamObserver<Empty>) { override fun swipe(
request: SwipeRequest,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
val from = request.from val from = request.from
val to = request.to val to = request.to
@@ -98,16 +122,25 @@ class DriverService(
} }
} }
override fun pressKey(request: PressKeyRequest, responseObserver: StreamObserver<Empty>) { override fun pressKey(
request: PressKeyRequest,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
backend.pressKey(request.key) backend.pressKey(request.key)
Empty.getDefaultInstance() Empty.getDefaultInstance()
} }
} }
override fun recentLogs(request: RecentLogsRequest, responseObserver: StreamObserver<LogEntries>) { override fun recentLogs(
request: RecentLogsRequest,
responseObserver: StreamObserver<LogEntries>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
val entries = backend.recentLogs(request.sinceUnixMillis, request.levelAtLeast) val entries = backend.recentLogs(
request.sinceUnixMillis,
request.levelAtLeast,
)
val builder = LogEntries.newBuilder() val builder = LogEntries.newBuilder()
for (entry in entries) { for (entry in entries) {
builder.addEntries( builder.addEntries(
@@ -123,7 +156,10 @@ class DriverService(
} }
} }
override fun screenshot(request: Empty, responseObserver: StreamObserver<Image>) { override fun screenshot(
request: Empty,
responseObserver: StreamObserver<Image>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
val (png, width, height) = backend.screenshot() val (png, width, height) = backend.screenshot()
Image.newBuilder() Image.newBuilder()
@@ -134,18 +170,28 @@ class DriverService(
} }
} }
override fun hierarchy(request: Empty, responseObserver: StreamObserver<HierarchyJSON>) { override fun hierarchy(
request: Empty,
responseObserver: StreamObserver<HierarchyJSON>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
HierarchyJSON.newBuilder().setJson(backend.hierarchy()).build() HierarchyJSON.newBuilder().setJson(backend.hierarchy()).build()
} }
} }
override fun snapshot(request: Empty, responseObserver: StreamObserver<SnapshotResponse>) { override fun snapshot(
request: Empty,
responseObserver: StreamObserver<SnapshotResponse>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
val sample = synchronized(snapshotLock) { backend.snapshot() } val sample = synchronized(snapshotLock) { backend.snapshot() }
val (png, width, height) = sample.screenshot val (png, width, height) = sample.screenshot
SnapshotResponse.newBuilder() SnapshotResponse.newBuilder()
.setHierarchy(HierarchyJSON.newBuilder().setJson(sample.hierarchyJson).build()) .setHierarchy(
HierarchyJSON.newBuilder()
.setJson(sample.hierarchyJson)
.build(),
)
.setScreenshot( .setScreenshot(
Image.newBuilder() Image.newBuilder()
.setPng(ByteString.copyFrom(png)) .setPng(ByteString.copyFrom(png))
@@ -157,14 +203,20 @@ class DriverService(
} }
} }
override fun waitForIdle(request: Duration, responseObserver: StreamObserver<Empty>) { override fun waitForIdle(
request: Duration,
responseObserver: StreamObserver<Empty>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
backend.waitForIdle(request.millis) backend.waitForIdle(request.millis)
Empty.getDefaultInstance() Empty.getDefaultInstance()
} }
} }
override fun health(request: Empty, responseObserver: StreamObserver<HealthStatus>) { override fun health(
request: Empty,
responseObserver: StreamObserver<HealthStatus>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
HealthStatus.newBuilder() HealthStatus.newBuilder()
.setReady(backend.healthy()) .setReady(backend.healthy())
@@ -174,9 +226,16 @@ class DriverService(
} }
} }
override fun metrics(request: MetricsRequest, responseObserver: StreamObserver<MetricsResponse>) { override fun metrics(
request: MetricsRequest,
responseObserver: StreamObserver<MetricsResponse>,
) {
runRpc(responseObserver) { runRpc(responseObserver) {
val bundleId = if (request.bundleId.isNotEmpty()) request.bundleId else launchedBundleId.get().orEmpty() val bundleId = if (request.bundleId.isNotEmpty()) {
request.bundleId
} else {
launchedBundleId.get().orEmpty()
}
val sample = backend.metrics(bundleId) val sample = backend.metrics(bundleId)
MetricsResponse.newBuilder() MetricsResponse.newBuilder()
.setCpuPercent(sample.cpuPercent) .setCpuPercent(sample.cpuPercent)
@@ -191,7 +250,9 @@ class DriverService(
// stale session, then closes the backend so the iOS XCTest runner process // stale session, then closes the backend so the iOS XCTest runner process
// dies with us instead of being orphaned. // dies with us instead of being orphaned.
fun shutdown() { fun shutdown() {
runCatching { launchedBundleId.getAndSet(null)?.let { backend.terminate(it) } } runCatching {
launchedBundleId.getAndSet(null)?.let { backend.terminate(it) }
}
runCatching { backend.close() } runCatching { backend.close() }
} }
@@ -209,8 +270,10 @@ class DriverService(
// failures that do not extend Exception, and an uncaught one // failures that do not extend Exception, and an uncaught one
// kills the RPC as a channel-level Unknown instead of a status // kills the RPC as a channel-level Unknown instead of a status
// the runner can classify. // the runner can classify.
observer.onError(io.grpc.Status.INTERNAL.withDescription(cause.toString()) observer.onError(
.withCause(cause).asRuntimeException()) io.grpc.Status.INTERNAL.withDescription(cause.toString())
.withCause(cause).asRuntimeException(),
)
} }
} }
@@ -13,14 +13,17 @@ class SidecarServer(
private val shutdownLatch = CountDownLatch(1) private val shutdownLatch = CountDownLatch(1)
fun start(): Int { fun start(): Int {
val server = NettyServerBuilder.forAddress(InetSocketAddress("127.0.0.1", port)) val server = NettyServerBuilder
.forAddress(InetSocketAddress("127.0.0.1", port))
.addService(service) .addService(service)
.build() .build()
server.start() server.start()
grpcServer = server grpcServer = server
Runtime.getRuntime().addShutdownHook(Thread { Runtime.getRuntime().addShutdownHook(
stop() Thread {
}) stop()
},
)
return server.port return server.port
} }
@@ -48,23 +51,41 @@ class SidecarServer(
// lost from run output. // lost from run output.
private fun quietExpectedDriverNoise() { private fun quietExpectedDriverNoise() {
org.apache.logging.log4j.core.config.Configurator.setLevel( org.apache.logging.log4j.core.config.Configurator.setLevel(
"util.CommandLineUtils", org.apache.logging.log4j.Level.OFF) "util.CommandLineUtils",
org.apache.logging.log4j.Level.OFF,
)
org.apache.logging.log4j.core.config.Configurator.setLevel( org.apache.logging.log4j.core.config.Configurator.setLevel(
"xcuitest.XCTestDriverClient", org.apache.logging.log4j.Level.OFF) "xcuitest.XCTestDriverClient",
org.apache.logging.log4j.Level.OFF,
)
org.apache.logging.log4j.core.config.Configurator.setLevel( org.apache.logging.log4j.core.config.Configurator.setLevel(
"maestro.drivers.AndroidDriver", org.apache.logging.log4j.Level.OFF) "maestro.drivers.AndroidDriver",
org.apache.logging.log4j.Level.OFF,
)
} }
fun main(arguments: Array<String>) { fun main(arguments: Array<String>) {
quietExpectedDriverNoise() quietExpectedDriverNoise()
val port = arguments.indexOf("--port").let { index -> val port = arguments.indexOf("--port").let { index ->
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1].toInt() else 0 if (index >= 0 && index + 1 < arguments.size) {
arguments[index + 1].toInt()
} else {
0
}
} }
val platform = arguments.indexOf("--platform").let { index -> val platform = arguments.indexOf("--platform").let { index ->
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else "android" if (index >= 0 && index + 1 < arguments.size) {
arguments[index + 1]
} else {
"android"
}
} }
val serial = arguments.indexOf("--serial").let { index -> val serial = arguments.indexOf("--serial").let { index ->
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else null if (index >= 0 && index + 1 < arguments.size) {
arguments[index + 1]
} else {
null
}
} }
val backend: DriverBackend = when (platform) { val backend: DriverBackend = when (platform) {
@@ -74,7 +95,9 @@ fun main(arguments: Array<String>) {
val service = DriverService(platform = platform, backend = backend) val service = DriverService(platform = platform, backend = backend)
val server = SidecarServer(port, service) val server = SidecarServer(port, service)
val boundPort = server.start() val boundPort = server.start()
println("sanderling-sidecar listening on 127.0.0.1:$boundPort platform=$platform") println(
"sanderling-sidecar listening on 127.0.0.1:$boundPort platform=$platform",
)
System.out.flush() System.out.flush()
server.awaitTermination() server.awaitTermination()
} }
@@ -10,7 +10,10 @@ class DadbTargetTest {
} }
@Test fun hostPortSerialConnectsDirectly() { @Test fun hostPortSerialConnectsDirectly() {
assertEquals(DadbTarget.Tcp("192.168.1.243", 5555), dadbTargetFor("192.168.1.243:5555")) assertEquals(
DadbTarget.Tcp("192.168.1.243", 5555),
dadbTargetFor("192.168.1.243:5555"),
)
} }
@Test fun usbSerialRoutesThroughAdbServer() { @Test fun usbSerialRoutesThroughAdbServer() {
@@ -20,6 +23,9 @@ class DadbTargetTest {
// A colon with a non-numeric port is a USB serial that merely contains a // A colon with a non-numeric port is a USB serial that merely contains a
// colon, not a host:port, so it must route through the adb server. // colon, not a host:port, so it must route through the adb server.
@Test fun colonWithNonNumericPortIsAServerSerial() { @Test fun colonWithNonNumericPortIsAServerSerial() {
assertEquals(DadbTarget.Server("emulator:5554x"), dadbTargetFor("emulator:5554x")) assertEquals(
DadbTarget.Server("emulator:5554x"),
dadbTargetFor("emulator:5554x"),
)
} }
} }
@@ -24,7 +24,8 @@ class DeviceOutputParserTest {
assertEquals("FATAL EXCEPTION: main", lines[0].message) assertEquals("FATAL EXCEPTION: main", lines[0].message)
val year = java.util.Calendar.getInstance().get(java.util.Calendar.YEAR) val year = java.util.Calendar.getInstance().get(java.util.Calendar.YEAR)
val cal = java.util.Calendar.getInstance().apply { timeInMillis = lines[0].unixMillis } val cal = java.util.Calendar.getInstance()
.apply { timeInMillis = lines[0].unixMillis }
assertEquals(year, cal.get(java.util.Calendar.YEAR)) assertEquals(year, cal.get(java.util.Calendar.YEAR))
assertEquals(56, cal.get(java.util.Calendar.SECOND)) assertEquals(56, cal.get(java.util.Calendar.SECOND))
assertEquals(789, cal.get(java.util.Calendar.MILLISECOND)) assertEquals(789, cal.get(java.util.Calendar.MILLISECOND))
@@ -51,7 +52,9 @@ class DeviceOutputParserTest {
@Test fun parseCpuTicksReturnsNullOnTruncatedOrNonNumericStat() { @Test fun parseCpuTicksReturnsNullOnTruncatedOrNonNumericStat() {
assertNull(parseCpuTicks("1234 (app) S 1 2 3")) assertNull(parseCpuTicks("1234 (app) S 1 2 3"))
assertNull(parseCpuTicks("1234 (app) S " + (1..12).joinToString(" ") { "x" })) assertNull(
parseCpuTicks("1234 (app) S " + (1..12).joinToString(" ") { "x" }),
)
assertNull(parseCpuTicks("")) assertNull(parseCpuTicks(""))
} }
@@ -79,8 +82,14 @@ class DeviceOutputParserTest {
} }
@Test fun parseBoundsAcceptsWellFormedAndRejectsMalformed() { @Test fun parseBoundsAcceptsWellFormedAndRejectsMalformed() {
assertEquals(listOf(0, 0, 1080, 2340), parseBounds("[0,0,1080,2340]")?.toList()) assertEquals(
assertEquals(listOf(-5, -10, 20, 30), parseBounds("[-5,-10,20,30]")?.toList()) listOf(0, 0, 1080, 2340),
parseBounds("[0,0,1080,2340]")?.toList(),
)
assertEquals(
listOf(-5, -10, 20, 30),
parseBounds("[-5,-10,20,30]")?.toList(),
)
assertNull(parseBounds("[0,0,1080]")) assertNull(parseBounds("[0,0,1080]"))
assertNull(parseBounds("0,0,1,1")) assertNull(parseBounds("0,0,1,1"))
assertNull(parseBounds("[0, 0, 1, 1]")) assertNull(parseBounds("[0, 0, 1, 1]"))
@@ -92,10 +101,14 @@ class DeviceOutputParserTest {
"resource-id" to "com.example:id/loginButton", "resource-id" to "com.example:id/loginButton",
"bounds" to "[10,20,110,80]", "bounds" to "[10,20,110,80]",
) )
assertEquals(listOf(10, 20, 110, 80), findBoundsBySelector(tree, "id:loginButton")?.toList())
assertEquals( assertEquals(
listOf(10, 20, 110, 80), listOf(10, 20, 110, 80),
findBoundsBySelector(tree, "id:com.example:id/loginButton")?.toList(), findBoundsBySelector(tree, "id:loginButton")?.toList(),
)
assertEquals(
listOf(10, 20, 110, 80),
findBoundsBySelector(tree, "id:com.example:id/loginButton")
?.toList(),
) )
} }
@@ -104,21 +117,36 @@ class DeviceOutputParserTest {
"resource-id" to "root", "resource-id" to "root",
children = listOf( children = listOf(
node("text" to "Sign in", "bounds" to "[1,2,3,4]"), node("text" to "Sign in", "bounds" to "[1,2,3,4]"),
node("content-desc" to "AccountCardRow-7", "bounds" to "[5,6,7,8]"), node(
"content-desc" to "AccountCardRow-7",
"bounds" to "[5,6,7,8]",
),
), ),
) )
assertEquals(listOf(1, 2, 3, 4), findBoundsBySelector(tree, "text:Sign in")?.toList()) assertEquals(
assertEquals(listOf(5, 6, 7, 8), findBoundsBySelector(tree, "descPrefix:AccountCard")?.toList()) listOf(1, 2, 3, 4),
findBoundsBySelector(tree, "text:Sign in")?.toList(),
)
assertEquals(
listOf(5, 6, 7, 8),
findBoundsBySelector(tree, "descPrefix:AccountCard")?.toList(),
)
} }
@Test fun findBoundsBySelectorReturnsNullForBadSelectorOrNoMatch() { @Test fun findBoundsBySelectorReturnsNullForBadSelectorOrNoMatch() {
val tree = node("resource-id" to "com.example:id/x", "bounds" to "[0,0,1,1]") val tree = node(
"resource-id" to "com.example:id/x",
"bounds" to "[0,0,1,1]",
)
assertNull(findBoundsBySelector(tree, "id")) assertNull(findBoundsBySelector(tree, "id"))
assertNull(findBoundsBySelector(tree, "id:missing")) assertNull(findBoundsBySelector(tree, "id:missing"))
} }
@Test fun findBoundsBySelectorReturnsNullWhenMatchHasMalformedBounds() { @Test fun findBoundsBySelectorReturnsNullWhenMatchHasMalformedBounds() {
val tree = node("resource-id" to "com.example:id/x", "bounds" to "not-bounds") val tree = node(
"resource-id" to "com.example:id/x",
"bounds" to "not-bounds",
)
assertNull(findBoundsBySelector(tree, "id:x")) assertNull(findBoundsBySelector(tree, "id:x"))
} }
@@ -132,17 +160,26 @@ class DeviceOutputParserTest {
private fun ihdr(width: Int, height: Int): ByteArray { private fun ihdr(width: Int, height: Int): ByteArray {
val b = ByteArray(33) val b = ByteArray(33)
for (i in 0 until 8) b[8 + i] = 0 for (i in 0 until 8) b[8 + i] = 0
b[12] = 'I'.code.toByte(); b[13] = 'H'.code.toByte() b[12] = 'I'.code.toByte()
b[14] = 'D'.code.toByte(); b[15] = 'R'.code.toByte() b[13] = 'H'.code.toByte()
b[16] = (width ushr 24).toByte(); b[17] = (width ushr 16).toByte() b[14] = 'D'.code.toByte()
b[18] = (width ushr 8).toByte(); b[19] = width.toByte() b[15] = 'R'.code.toByte()
b[20] = (height ushr 24).toByte(); b[21] = (height ushr 16).toByte() b[16] = (width ushr 24).toByte()
b[22] = (height ushr 8).toByte(); b[23] = height.toByte() b[17] = (width ushr 16).toByte()
b[18] = (width ushr 8).toByte()
b[19] = width.toByte()
b[20] = (height ushr 24).toByte()
b[21] = (height ushr 16).toByte()
b[22] = (height ushr 8).toByte()
b[23] = height.toByte()
return b return b
} }
private fun node( private fun node(
vararg attrs: Pair<String, String>, vararg attrs: Pair<String, String>,
children: List<maestro.TreeNode> = emptyList(), children: List<maestro.TreeNode> = emptyList(),
): maestro.TreeNode = maestro.TreeNode(attributes = attrs.toMap().toMutableMap(), children = children) ): maestro.TreeNode = maestro.TreeNode(
attributes = attrs.toMap().toMutableMap(),
children = children,
)
} }
@@ -20,20 +20,34 @@ import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
private data class Quintuple<A, B, C, D, E>(val a: A, val b: B, val c: C, val d: D, val e: E) private data class Quintuple<A, B, C, D, E>(
val a: A,
val b: B,
val c: C,
val d: D,
val e: E,
)
class DriverServiceTest { class DriverServiceTest {
@get:Rule val grpcCleanup: GrpcCleanupRule = GrpcCleanupRule() @get:Rule val grpcCleanup: GrpcCleanupRule = GrpcCleanupRule()
private fun newClient(backend: DriverBackend): DriverGrpc.DriverBlockingStub { private fun newClient(
backend: DriverBackend,
): DriverGrpc.DriverBlockingStub {
val serverName = InProcessServerBuilder.generateName() val serverName = InProcessServerBuilder.generateName()
val service = DriverService(platform = "android", backend = backend) val service = DriverService(platform = "android", backend = backend)
grpcCleanup.register( grpcCleanup.register(
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start() InProcessServerBuilder.forName(serverName)
.directExecutor()
.addService(service)
.build()
.start(),
) )
val channel: ManagedChannel = grpcCleanup.register( val channel: ManagedChannel = grpcCleanup.register(
InProcessChannelBuilder.forName(serverName).directExecutor().build() InProcessChannelBuilder.forName(serverName)
.directExecutor()
.build(),
) )
return DriverGrpc.newBlockingStub(channel) return DriverGrpc.newBlockingStub(channel)
} }
@@ -59,20 +73,32 @@ class DriverServiceTest {
var terminated: String? = null var terminated: String? = null
var closed = false var closed = false
val backend = object : DriverBackend by StubDriverBackend("android") { val backend = object : DriverBackend by StubDriverBackend("android") {
override fun terminate(bundleId: String) { terminated = bundleId } override fun terminate(bundleId: String) {
override fun close() { closed = true } terminated = bundleId
}
override fun close() {
closed = true
}
} }
val serverName = InProcessServerBuilder.generateName() val serverName = InProcessServerBuilder.generateName()
val service = DriverService(platform = "android", backend = backend) val service = DriverService(platform = "android", backend = backend)
grpcCleanup.register( grpcCleanup.register(
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start() InProcessServerBuilder.forName(serverName)
.directExecutor()
.addService(service)
.build()
.start(),
) )
val channel: ManagedChannel = grpcCleanup.register( val channel: ManagedChannel = grpcCleanup.register(
InProcessChannelBuilder.forName(serverName).directExecutor().build() InProcessChannelBuilder.forName(serverName)
.directExecutor()
.build(),
) )
val client = DriverGrpc.newBlockingStub(channel) val client = DriverGrpc.newBlockingStub(channel)
client.launch(LaunchRequest.newBuilder().setBundleId("com.example").build()) client.launch(
LaunchRequest.newBuilder().setBundleId("com.example").build(),
)
service.shutdown() service.shutdown()
assertEquals("com.example", terminated) assertEquals("com.example", terminated)
@@ -83,8 +109,12 @@ class DriverServiceTest {
var terminated: String? = null var terminated: String? = null
var closed = false var closed = false
val backend = object : DriverBackend by StubDriverBackend("android") { val backend = object : DriverBackend by StubDriverBackend("android") {
override fun terminate(bundleId: String) { terminated = bundleId } override fun terminate(bundleId: String) {
override fun close() { closed = true } terminated = bundleId
}
override fun close() {
closed = true
}
} }
val service = DriverService(platform = "android", backend = backend) val service = DriverService(platform = "android", backend = backend)
@@ -128,17 +158,17 @@ class DriverServiceTest {
// the runner can tell transient failures from fatal ones. // the runner can tell transient failures from fatal ones.
@Test fun backendStatusCodePassesThrough() { @Test fun backendStatusCodePassesThrough() {
val backend = object : DriverBackend by StubDriverBackend("android") { val backend = object : DriverBackend by StubDriverBackend("android") {
override fun inputText(text: String) { override fun inputText(text: String): Unit =
throw io.grpc.Status.UNAVAILABLE throw io.grpc.Status.UNAVAILABLE
.withDescription("connection dropped mid-action") .withDescription("connection dropped mid-action")
.asRuntimeException() .asRuntimeException()
}
} }
val client = newClient(backend) val client = newClient(backend)
val thrown = kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> { val thrown =
client.inputText(Text.newBuilder().setValue("hello").build()) kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
} client.inputText(Text.newBuilder().setValue("hello").build())
}
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code) assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
} }
@@ -147,17 +177,19 @@ class DriverServiceTest {
// channel-level Unknown the runner cannot classify. // channel-level Unknown the runner cannot classify.
@Test fun nonExceptionThrowableMapsToInternal() { @Test fun nonExceptionThrowableMapsToInternal() {
val backend = object : DriverBackend by StubDriverBackend("android") { val backend = object : DriverBackend by StubDriverBackend("android") {
override fun inputText(text: String) { override fun inputText(text: String): Unit =
throw Throwable("only one gesture can be performed at a time") throw Throwable("only one gesture can be performed at a time")
}
} }
val client = newClient(backend) val client = newClient(backend)
val thrown = kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> { val thrown =
client.inputText(Text.newBuilder().setValue("hello").build()) kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
} client.inputText(Text.newBuilder().setValue("hello").build())
}
assertEquals(io.grpc.Status.Code.INTERNAL, thrown.status.code) assertEquals(io.grpc.Status.Code.INTERNAL, thrown.status.code)
assertTrue(thrown.status.description.orEmpty().contains("only one gesture")) assertTrue(
thrown.status.description.orEmpty().contains("only one gesture"),
)
} }
@Test fun reapOrphanIosRunnersKillsStrayXcodebuildAndRunnerApp() { @Test fun reapOrphanIosRunnersKillsStrayXcodebuildAndRunnerApp() {
@@ -171,7 +203,16 @@ class DriverServiceTest {
assertEquals("pkill", commands[0][0]) assertEquals("pkill", commands[0][0])
assertTrue(commands[0][2].contains("test-without-building")) assertTrue(commands[0][2].contains("test-without-building"))
assertTrue(commands[0][2].contains("UDID-1234")) assertTrue(commands[0][2].contains("UDID-1234"))
assertEquals(listOf("xcrun", "simctl", "terminate", "UDID-1234", IOS_XCTEST_RUNNER_BUNDLE_ID), commands[1]) assertEquals(
listOf(
"xcrun",
"simctl",
"terminate",
"UDID-1234",
IOS_XCTEST_RUNNER_BUNDLE_ID,
),
commands[1],
)
} }
@Test fun reapOrphanIosRunnersReportsNothingFound() { @Test fun reapOrphanIosRunnersReportsNothingFound() {
@@ -192,7 +233,9 @@ class DriverServiceTest {
// still executing fails instead of queuing. // still executing fails instead of queuing.
val tapAction = { val tapAction = {
if (!inFlight.compareAndSet(false, true)) { if (!inFlight.compareAndSet(false, true)) {
throw IllegalStateException("only one gesture can be performed at a time") throw IllegalStateException(
"only one gesture can be performed at a time",
)
} }
invocations.incrementAndGet() invocations.incrementAndGet()
Thread.sleep(150) Thread.sleep(150)
@@ -211,7 +254,9 @@ class DriverServiceTest {
val tapAction = { val tapAction = {
if (failedFirst.compareAndSet(false, true)) { if (failedFirst.compareAndSet(false, true)) {
Thread.sleep(60) Thread.sleep(60)
throw IllegalStateException("only one gesture can be performed at a time") throw IllegalStateException(
"only one gesture can be performed at a time",
)
} }
landed.incrementAndGet() landed.incrementAndGet()
Unit Unit
@@ -226,21 +271,38 @@ class DriverServiceTest {
// directly. // directly.
val taps = mutableListOf<Pair<Int, Int>>() val taps = mutableListOf<Pair<Int, Int>>()
val backend = object : DriverBackend { val backend = object : DriverBackend {
override fun launch(bundleId: String, clearState: Boolean, env: Map<String, String>) {} override fun launch(
bundleId: String,
clearState: Boolean,
env: Map<String, String>,
) {}
override fun terminate(bundleId: String) {} override fun terminate(bundleId: String) {}
override fun tap(x: Int, y: Int) { taps.add(x to y) } override fun tap(x: Int, y: Int) {
taps.add(x to y)
}
override fun tapSelector(selector: String) {} override fun tapSelector(selector: String) {}
override fun inputText(text: String) {} override fun inputText(text: String) {}
override fun eraseText(characterCount: Int) {} override fun eraseText(characterCount: Int) {}
override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) {} override fun swipe(
fromX: Int,
fromY: Int,
toX: Int,
toY: Int,
durationMillis: Long,
) {}
override fun pressKey(key: String) {} override fun pressKey(key: String) {}
override fun longPress(x: Int, y: Int) {} override fun longPress(x: Int, y: Int) {}
override fun screenshot(): Triple<ByteArray, Int, Int> = Triple(byteArrayOf(), 0, 0) override fun screenshot(): Triple<ByteArray, Int, Int> =
Triple(byteArrayOf(), 0, 0)
override fun hierarchy(): String = "{}" override fun hierarchy(): String = "{}"
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> = emptyList() override fun recentLogs(
sinceUnixMillis: Long,
minLevel: String,
): List<LogLine> = emptyList()
override fun waitForIdle(durationMillis: Long) {} override fun waitForIdle(durationMillis: Long) {}
override fun healthy(): Boolean = true override fun healthy(): Boolean = true
override fun metrics(bundleId: String): MetricsSample = MetricsSample(0.0, 0L, 0L) override fun metrics(bundleId: String): MetricsSample =
MetricsSample(0.0, 0L, 0L)
} }
val client = newClient(backend) val client = newClient(backend)
@@ -252,13 +314,16 @@ class DriverServiceTest {
val backend = StubDriverBackend("android") val backend = StubDriverBackend("android")
val client = newClient(backend) val client = newClient(backend)
client.eraseText(EraseTextRequest.newBuilder().setCharacterCount(11).build()) client.eraseText(
EraseTextRequest.newBuilder().setCharacterCount(11).build(),
)
assertEquals(11, backend.lastEraseCharacterCount) assertEquals(11, backend.lastEraseCharacterCount)
} }
@Test fun screenshotReturnsBackendBytes() { @Test fun screenshotReturnsBackendBytes() {
val backend = object : DriverBackend by StubDriverBackend("android") { val backend = object : DriverBackend by StubDriverBackend("android") {
override fun screenshot(): Triple<ByteArray, Int, Int> = Triple(byteArrayOf(1, 2, 3), 1080, 2340) override fun screenshot(): Triple<ByteArray, Int, Int> =
Triple(byteArrayOf(1, 2, 3), 1080, 2340)
} }
val client = newClient(backend) val client = newClient(backend)
@@ -294,7 +359,13 @@ class DriverServiceTest {
@Test fun swipeForwardsEndpointsAndDuration() { @Test fun swipeForwardsEndpointsAndDuration() {
var observed: Quintuple<Int, Int, Int, Int, Long>? = null var observed: Quintuple<Int, Int, Int, Int, Long>? = null
val backend = object : DriverBackend by StubDriverBackend("android") { val backend = object : DriverBackend by StubDriverBackend("android") {
override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) { override fun swipe(
fromX: Int,
fromY: Int,
toX: Int,
toY: Int,
durationMillis: Long,
) {
observed = Quintuple(fromX, fromY, toX, toY, durationMillis) observed = Quintuple(fromX, fromY, toX, toY, durationMillis)
} }
} }
@@ -325,14 +396,18 @@ class DriverServiceTest {
@Test fun recentLogsReturnsBackendEntries() { @Test fun recentLogsReturnsBackendEntries() {
val backend = object : DriverBackend by StubDriverBackend("android") { val backend = object : DriverBackend by StubDriverBackend("android") {
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> { override fun recentLogs(
return listOf(LogLine(1, "E", "AndroidRuntime", "boom")) sinceUnixMillis: Long,
} minLevel: String,
): List<LogLine> = listOf(LogLine(1, "E", "AndroidRuntime", "boom"))
} }
val client = newClient(backend) val client = newClient(backend)
val response = client.recentLogs( val response = client.recentLogs(
RecentLogsRequest.newBuilder().setSinceUnixMillis(0).setLevelAtLeast("E").build(), RecentLogsRequest.newBuilder()
.setSinceUnixMillis(0)
.setLevelAtLeast("E")
.build(),
) )
assertEquals(1, response.entriesCount) assertEquals(1, response.entriesCount)
assertEquals("AndroidRuntime", response.getEntries(0).tag) assertEquals("AndroidRuntime", response.getEntries(0).tag)
@@ -351,7 +426,9 @@ class DriverServiceTest {
} }
val client = newClient(backend) val client = newClient(backend)
client.launch(LaunchRequest.newBuilder().setBundleId("com.launched").build()) client.launch(
LaunchRequest.newBuilder().setBundleId("com.launched").build(),
)
client.metrics(MetricsRequest.getDefaultInstance()) client.metrics(MetricsRequest.getDefaultInstance())
assertEquals("com.launched", sampled) assertEquals("com.launched", sampled)
@@ -367,8 +444,12 @@ class DriverServiceTest {
} }
val client = newClient(backend) val client = newClient(backend)
client.launch(LaunchRequest.newBuilder().setBundleId("com.launched").build()) client.launch(
client.metrics(MetricsRequest.newBuilder().setBundleId("com.other").build()) LaunchRequest.newBuilder().setBundleId("com.launched").build(),
)
client.metrics(
MetricsRequest.newBuilder().setBundleId("com.other").build(),
)
assertEquals("com.other", sampled) assertEquals("com.other", sampled)
} }
@@ -32,7 +32,8 @@ class InputTextTest {
val fallback = listOf( val fallback = listOf(
"Emergency Fund", "🙂🔥💸", " ", "\t\n", "'; DROP TABLE--", "Emergency Fund", "🙂🔥💸", " ", "\t\n", "'; DROP TABLE--",
"<script>alert(1)</script>", "../../etc/passwd", "%s%n", "", "<script>alert(1)</script>", "../../etc/passwd", "%s%n", "",
"-1", "-rf", // a leading dash could be read as an option by `input text` // a leading dash could be read as an option by `input text`
"-1", "-rf",
) )
for (text in fallback) { for (text in fallback) {
assertTrue( assertTrue(
@@ -190,12 +191,12 @@ class InputTextTest {
@Test fun parseResumedPackageReadsEachResumedActivityWording() { @Test fun parseResumedPackageReadsEachResumedActivityWording() {
val cases = mapOf( val cases = mapOf(
" topResumedActivity=ActivityRecord{8b u0 app.folio/.MainActivity t42}" to " topResumedActivity=ActivityRecord{8b u0 " +
"app.folio", "app.folio/.MainActivity t42}" to "app.folio",
" mResumedActivity: ActivityRecord{1c u0 com.example.app/.Home t9}" to " mResumedActivity: ActivityRecord{1c u0 " +
"com.example.app", "com.example.app/.Home t9}" to "com.example.app",
" ResumedActivity: ActivityRecord{2d u0 app.folio/com.folio.Detail t9}" to " ResumedActivity: ActivityRecord{2d u0 " +
"app.folio", "app.folio/com.folio.Detail t9}" to "app.folio",
) )
for ((line, want) in cases) { for ((line, want) in cases) {
assertEquals(want, parseResumedPackage(line), line) assertEquals(want, parseResumedPackage(line), line)
@@ -12,28 +12,40 @@ class ResolveActivityTest {
com.example.app/.MainActivity com.example.app/.MainActivity
""".trimIndent() """.trimIndent()
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output) val activity = StubDriverBackend.parseResolvedActivity(
"com.example.app",
output,
)
assertEquals(".MainActivity", activity) assertEquals(".MainActivity", activity)
} }
@Test fun extractsFullyQualifiedActivity() { @Test fun extractsFullyQualifiedActivity() {
val output = "com.example.app/com.example.app.ui.LaunchActivity" val output = "com.example.app/com.example.app.ui.LaunchActivity"
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output) val activity = StubDriverBackend.parseResolvedActivity(
"com.example.app",
output,
)
assertEquals("com.example.app.ui.LaunchActivity", activity) assertEquals("com.example.app.ui.LaunchActivity", activity)
} }
@Test fun returnsNullWhenPackageNotFound() { @Test fun returnsNullWhenPackageNotFound() {
val output = "No activity found" val output = "No activity found"
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output) val activity = StubDriverBackend.parseResolvedActivity(
"com.example.app",
output,
)
assertNull(activity) assertNull(activity)
} }
@Test fun doesNotMatchDifferentPackagePrefix() { @Test fun doesNotMatchDifferentPackagePrefix() {
val output = "other.pkg/.MainActivity" val output = "other.pkg/.MainActivity"
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output) val activity = StubDriverBackend.parseResolvedActivity(
"com.example.app",
output,
)
assertNull(activity) assertNull(activity)
} }
} }
@@ -13,10 +13,13 @@ class RouteTransitionTest {
private fun screen(id: String, child: String = "") = private fun screen(id: String, child: String = "") =
"""{"attributes":{"resource-id":"$id"},"children":[$child]}""" """{"attributes":{"resource-id":"$id"},"children":[$child]}"""
private fun tree(vararg children: String) = private fun tree(vararg children: String): String {
"""{"attributes":{"resource-id":"root"},"children":[${children.joinToString(",")}]}""" val joined = children.joinToString(",")
return """{"attributes":{"resource-id":"root"},"children":[$joined]}"""
}
private val crossFade = tree(screen("LedgerScreen"), screen("AddTransactionScreen")) private val crossFade =
tree(screen("LedgerScreen"), screen("AddTransactionScreen"))
private val landed = tree(screen("AddTransactionScreen")) private val landed = tree(screen("AddTransactionScreen"))
@Test fun waitsForTheCrossFadeToLandAndReturnsTheLandedTree() { @Test fun waitsForTheCrossFadeToLandAndReturnsTheLandedTree() {
@@ -29,8 +32,15 @@ class RouteTransitionTest {
reads++ reads++
if (reads <= 3) crossFade else landed if (reads <= 3) crossFade else landed
} }
assertTrue(reads > 3, "must keep reading until the fade lands, reads=$reads") assertTrue(
assertEquals(1, countRouteScreens(settled), "must return a tree with one route") reads > 3,
"must keep reading until the fade lands, reads=$reads",
)
assertEquals(
1,
countRouteScreens(settled),
"must return a tree with one route",
)
} }
@Test fun settledFrameCostsExactlyOneRead() { @Test fun settledFrameCostsExactlyOneRead() {
@@ -54,13 +64,21 @@ class RouteTransitionTest {
// would burn the whole poll budget and still hand over a frame the // would burn the whole poll budget and still hand over a frame the
// runner refuses to act on. // runner refuses to act on.
val nested = tree(screen("HomeScreen", screen("HomeScreen"))) val nested = tree(screen("HomeScreen", screen("HomeScreen")))
assertEquals(1, countRouteScreens(nested), "the same id twice is one route") assertEquals(
1,
countRouteScreens(nested),
"the same id twice is one route",
)
var reads = 0 var reads = 0
awaitSettledTree { awaitSettledTree {
reads++ reads++
nested nested
} }
assertEquals(1, reads, "a repeated route id must not be treated as a transition") assertEquals(
1,
reads,
"a repeated route id must not be treated as a transition",
)
} }
@Test fun aLayoutThatKeepsTwoRoutesIsBoundedByTheCap() { @Test fun aLayoutThatKeepsTwoRoutesIsBoundedByTheCap() {
@@ -78,7 +96,11 @@ class RouteTransitionTest {
elapsed < TRANSITION_POLL_CAP_MILLIS + 1000L, elapsed < TRANSITION_POLL_CAP_MILLIS + 1000L,
"must stop at the cap, elapsed=${elapsed}ms", "must stop at the cap, elapsed=${elapsed}ms",
) )
assertEquals(crossFade, settled, "the caller still gets a tree to record") assertEquals(
crossFade,
settled,
"the caller still gets a tree to record",
)
} }
@Test fun capCoversTheNavHostFadePlusTheStreak() { @Test fun capCoversTheNavHostFadePlusTheStreak() {
@@ -89,20 +111,29 @@ class RouteTransitionTest {
val fadeMillis = 700L val fadeMillis = 700L
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val settled = awaitSettledTree { val settled = awaitSettledTree {
if (System.currentTimeMillis() - start < fadeMillis) crossFade else landed if (System.currentTimeMillis() - start < fadeMillis) {
crossFade
} else {
landed
}
} }
val elapsed = System.currentTimeMillis() - start val elapsed = System.currentTimeMillis() - start
assertEquals(landed, settled, "must hand back the landed tree, not the fade") assertEquals(
landed,
settled,
"must hand back the landed tree, not the fade",
)
assertTrue( assertTrue(
elapsed >= fadeMillis, elapsed >= fadeMillis,
"cannot have settled before the fade ended, elapsed=${elapsed}ms", "cannot have settled before the fade ended, elapsed=${elapsed}ms",
) )
assertTrue( assertTrue(
elapsed < TRANSITION_POLL_CAP_MILLIS, elapsed < TRANSITION_POLL_CAP_MILLIS,
"the ${TRANSITION_POLL_CAP_MILLIS}ms cap has to leave room for a ${fadeMillis}ms " + "the ${TRANSITION_POLL_CAP_MILLIS}ms cap has to leave room for " +
"fade and the ${TRANSITION_STABLE_STREAK_MILLIS}ms streak after it, but the " + "a ${fadeMillis}ms fade and the " +
"wait ran to the cap instead, elapsed=${elapsed}ms", "${TRANSITION_STABLE_STREAK_MILLIS}ms streak after it, but " +
"the wait ran to the cap instead, elapsed=${elapsed}ms",
) )
} }
} }
@@ -6,7 +6,10 @@ import kotlin.test.assertTrue
class SidecarServerTest { class SidecarServerTest {
@Test @Test
fun startBindsEphemeralPortAndStopReleasesIt() { fun startBindsEphemeralPortAndStopReleasesIt() {
val server = SidecarServer(port = 0, service = DriverService(backend = StubDriverBackend("android"))) val server = SidecarServer(
port = 0,
service = DriverService(backend = StubDriverBackend("android")),
)
val boundPort = server.start() val boundPort = server.start()
try { try {
assertTrue(boundPort > 0, "expected ephemeral port, got $boundPort") assertTrue(boundPort > 0, "expected ephemeral port, got $boundPort")
@@ -19,14 +19,22 @@ class SnapshotHandlerTest {
@get:Rule val grpcCleanup: GrpcCleanupRule = GrpcCleanupRule() @get:Rule val grpcCleanup: GrpcCleanupRule = GrpcCleanupRule()
private fun newClient(backend: DriverBackend): DriverGrpc.DriverBlockingStub { private fun newClient(
backend: DriverBackend,
): DriverGrpc.DriverBlockingStub {
val serverName = InProcessServerBuilder.generateName() val serverName = InProcessServerBuilder.generateName()
val service = DriverService(platform = "android", backend = backend) val service = DriverService(platform = "android", backend = backend)
grpcCleanup.register( grpcCleanup.register(
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start(), InProcessServerBuilder.forName(serverName)
.directExecutor()
.addService(service)
.build()
.start(),
) )
val channel: ManagedChannel = grpcCleanup.register( val channel: ManagedChannel = grpcCleanup.register(
InProcessChannelBuilder.forName(serverName).directExecutor().build(), InProcessChannelBuilder.forName(serverName)
.directExecutor()
.build(),
) )
return DriverGrpc.newBlockingStub(channel) return DriverGrpc.newBlockingStub(channel)
} }
@@ -37,8 +45,10 @@ class SnapshotHandlerTest {
// forward those calls to the delegate, not these overrides. Override // forward those calls to the delegate, not these overrides. Override
// snapshot() directly so the test exercises the wire path end-to-end. // snapshot() directly so the test exercises the wire path end-to-end.
val backend = object : DriverBackend by StubDriverBackend("android") { val backend = object : DriverBackend by StubDriverBackend("android") {
override fun snapshot(): SnapshotSample = override fun snapshot(): SnapshotSample = SnapshotSample(
SnapshotSample("{\"x\":1}", Triple(byteArrayOf(7, 8, 9), 1080, 2340)) "{\"x\":1}",
Triple(byteArrayOf(7, 8, 9), 1080, 2340),
)
} }
val client = newClient(backend) val client = newClient(backend)
@@ -55,13 +65,23 @@ class SnapshotHandlerTest {
// aligned with the final hierarchy snapshot the runner accepts. // aligned with the final hierarchy snapshot the runner accepts.
val callOrder = mutableListOf<String>() val callOrder = mutableListOf<String>()
val backend = object : DriverBackend { val backend = object : DriverBackend {
override fun launch(bundleId: String, clearState: Boolean, env: Map<String, String>) {} override fun launch(
bundleId: String,
clearState: Boolean,
env: Map<String, String>,
) {}
override fun terminate(bundleId: String) {} override fun terminate(bundleId: String) {}
override fun tap(x: Int, y: Int) {} override fun tap(x: Int, y: Int) {}
override fun tapSelector(selector: String) {} override fun tapSelector(selector: String) {}
override fun inputText(text: String) {} override fun inputText(text: String) {}
override fun eraseText(characterCount: Int) {} override fun eraseText(characterCount: Int) {}
override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) {} override fun swipe(
fromX: Int,
fromY: Int,
toX: Int,
toY: Int,
durationMillis: Long,
) {}
override fun pressKey(key: String) {} override fun pressKey(key: String) {}
override fun longPress(x: Int, y: Int) {} override fun longPress(x: Int, y: Int) {}
override fun screenshot(): Triple<ByteArray, Int, Int> { override fun screenshot(): Triple<ByteArray, Int, Int> {
@@ -72,10 +92,14 @@ class SnapshotHandlerTest {
callOrder.add("hierarchy") callOrder.add("hierarchy")
return "{}" return "{}"
} }
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> = emptyList() override fun recentLogs(
sinceUnixMillis: Long,
minLevel: String,
): List<LogLine> = emptyList()
override fun waitForIdle(durationMillis: Long) {} override fun waitForIdle(durationMillis: Long) {}
override fun healthy(): Boolean = true override fun healthy(): Boolean = true
override fun metrics(bundleId: String): MetricsSample = MetricsSample(0.0, 0L, 0L) override fun metrics(bundleId: String): MetricsSample =
MetricsSample(0.0, 0L, 0L)
} }
backend.snapshot() backend.snapshot()
assertEquals(listOf("hierarchy", "screenshot"), callOrder) assertEquals(listOf("hierarchy", "screenshot"), callOrder)
@@ -88,7 +112,8 @@ class SnapshotHandlerTest {
val maxObserved = AtomicInteger(0) val maxObserved = AtomicInteger(0)
val callCount = AtomicInteger(0) val callCount = AtomicInteger(0)
val lock = ReentrantLock() val lock = ReentrantLock()
val recordingBackend = object : DriverBackend by StubDriverBackend("android") { val delegate = StubDriverBackend("android")
val recordingBackend = object : DriverBackend by delegate {
override fun snapshot(): SnapshotSample { override fun snapshot(): SnapshotSample {
val now = inFlight.incrementAndGet() val now = inFlight.incrementAndGet()
try { try {
@@ -109,9 +134,13 @@ class SnapshotHandlerTest {
// Use a real (multi-threaded) executor on the server side so the service // Use a real (multi-threaded) executor on the server side so the service
// is not artificially serialized by directExecutor. // is not artificially serialized by directExecutor.
val serverName = InProcessServerBuilder.generateName() val serverName = InProcessServerBuilder.generateName()
val service = DriverService(platform = "android", backend = recordingBackend) val service =
DriverService(platform = "android", backend = recordingBackend)
grpcCleanup.register( grpcCleanup.register(
InProcessServerBuilder.forName(serverName).addService(service).build().start(), InProcessServerBuilder.forName(serverName)
.addService(service)
.build()
.start(),
) )
val channel: ManagedChannel = grpcCleanup.register( val channel: ManagedChannel = grpcCleanup.register(
InProcessChannelBuilder.forName(serverName).build(), InProcessChannelBuilder.forName(serverName).build(),
@@ -13,7 +13,10 @@ class StabilityPollTest {
elapsed >= MIN_STABLE_STREAK_MILLIS, elapsed >= MIN_STABLE_STREAK_MILLIS,
"must observe a stable streak of at least ${MIN_STABLE_STREAK_MILLIS}ms, elapsed=${elapsed}ms", "must observe a stable streak of at least ${MIN_STABLE_STREAK_MILLIS}ms, elapsed=${elapsed}ms",
) )
assertTrue(elapsed < 3000L, "should not run to cap when stable, elapsed=${elapsed}ms") assertTrue(
elapsed < 3000L,
"should not run to cap when stable, elapsed=${elapsed}ms",
)
} }
@Test fun slowSnapshotReadsDoNotEatTheStreak() { @Test fun slowSnapshotReadsDoNotEatTheStreak() {
@@ -38,8 +41,9 @@ class StabilityPollTest {
val observedQuiet = sampleStarts.last() - sampleEnds.first() val observedQuiet = sampleStarts.last() - sampleEnds.first()
assertTrue( assertTrue(
observedQuiet >= MIN_STABLE_STREAK_MILLIS, observedQuiet >= MIN_STABLE_STREAK_MILLIS,
"the poll returned having observed only ${observedQuiet}ms of quiet, not " + "the poll returned having observed only ${observedQuiet}ms of " +
"${MIN_STABLE_STREAK_MILLIS}ms; starts=$sampleStarts ends=$sampleEnds", "quiet, not ${MIN_STABLE_STREAK_MILLIS}ms; " +
"starts=$sampleStarts ends=$sampleEnds",
) )
assertTrue( assertTrue(
sampleStarts.size >= 3, sampleStarts.size >= 3,
@@ -60,23 +64,27 @@ class StabilityPollTest {
calls++ calls++
when { when {
calls <= 2 -> "calm" calls <= 2 -> "calm"
calls == 3 -> { calls == 3 -> {
transientAt = System.currentTimeMillis() transientAt = System.currentTimeMillis()
"transient" "transient"
} }
else -> "stable" else -> "stable"
} }
} }
val sinceTransition = System.currentTimeMillis() - transientAt val sinceTransition = System.currentTimeMillis() - transientAt
assertTrue( assertTrue(
calls >= 8, calls >= 8,
"after the transition the poll needs a fresh matching pair and then a full " + "after the transition the poll needs a fresh matching pair and " +
"${MIN_STABLE_STREAK_MILLIS}ms of quiet, which is 8 samples, got $calls", "then a full ${MIN_STABLE_STREAK_MILLIS}ms of quiet, which " +
"is 8 samples, got $calls",
) )
assertTrue( assertTrue(
sinceTransition >= MIN_STABLE_STREAK_MILLIS, sinceTransition >= MIN_STABLE_STREAK_MILLIS,
"the calm prefix must not count: a full ${MIN_STABLE_STREAK_MILLIS}ms streak has to " + "the calm prefix must not count: a full " +
"start over after the transition, returned ${sinceTransition}ms after it", "${MIN_STABLE_STREAK_MILLIS}ms streak has to start over " +
"after the transition, returned ${sinceTransition}ms after it",
) )
} }
@@ -105,7 +113,10 @@ class StabilityPollTest {
"frame-$calls" "frame-$calls"
} }
val elapsed = System.currentTimeMillis() - start val elapsed = System.currentTimeMillis() - start
assertTrue(elapsed in budget..(budget + 1000L), "expected to hit cap, elapsed=$elapsed") assertTrue(
elapsed in budget..(budget + 1000L),
"expected to hit cap, elapsed=$elapsed",
)
} }
@Test fun zeroBudgetReturnsImmediately() { @Test fun zeroBudgetReturnsImmediately() {
@@ -117,7 +128,8 @@ class StabilityPollTest {
assertEquals(0, calls) assertEquals(0, calls)
} }
@Test fun structuralHashIgnoresBoundsAndIdenticalForSemanticallyEqualTrees() { @Test
fun structuralHashIgnoresBoundsAndIdenticalForSemanticallyEqualTrees() {
val a = """ val a = """
{"attributes":{"resource-id":"LoginScreen","bounds":"[0,0,1080,2340]"}, {"attributes":{"resource-id":"LoginScreen","bounds":"[0,0,1080,2340]"},
"children":[ "children":[
@@ -130,13 +142,24 @@ class StabilityPollTest {
{"attributes":{"resource-id":"LoginEmail","bounds":"[10,11,1070,101]","text":"a@b"},"children":[]} {"attributes":{"resource-id":"LoginEmail","bounds":"[10,11,1070,101]","text":"a@b"},"children":[]}
]} ]}
""".trimIndent() """.trimIndent()
assertEquals(structuralHash(a), structuralHash(b), "bounds-only flicker must not change hash") assertEquals(
structuralHash(a),
structuralHash(b),
"bounds-only flicker must not change hash",
)
} }
@Test fun structuralHashDiffersWhenContentChanges() { @Test fun structuralHashDiffersWhenContentChanges() {
val a = """{"attributes":{"resource-id":"LoginEmail","text":"a@b"},"children":[]}""" val a = """
val b = """{"attributes":{"resource-id":"LoginEmail","text":"c@d"},"children":[]}""" {"attributes":{"resource-id":"LoginEmail","text":"a@b"},"children":[]}
assertTrue(structuralHash(a) != structuralHash(b), "text change must alter hash") """.trimIndent()
val b = """
{"attributes":{"resource-id":"LoginEmail","text":"c@d"},"children":[]}
""".trimIndent()
assertTrue(
structuralHash(a) != structuralHash(b),
"text change must alter hash",
)
} }
@Test fun stabilitySnapshotReturnsNullDuringNavHostCrossFade() { @Test fun stabilitySnapshotReturnsNullDuringNavHostCrossFade() {
@@ -160,7 +183,10 @@ class StabilityPollTest {
]} ]}
""".trimIndent() """.trimIndent()
val hash = stabilitySnapshot(singleScreen) val hash = stabilitySnapshot(singleScreen)
assertTrue(hash != null && hash.isNotBlank(), "single-screen tree must yield a hash, got $hash") assertTrue(
hash != null && hash.isNotBlank(),
"single-screen tree must yield a hash, got $hash",
)
} }
@Test fun stabilitySnapshotIgnoresNonRouteAttributeValues() { @Test fun stabilitySnapshotIgnoresNonRouteAttributeValues() {
@@ -173,7 +199,10 @@ class StabilityPollTest {
{"attributes":{"text":"Welcome to MyScreen"},"children":[]} {"attributes":{"text":"Welcome to MyScreen"},"children":[]}
]} ]}
""".trimIndent() """.trimIndent()
assertTrue(stabilitySnapshot(tree) != null, "non-route attribute must not be counted as a screen") assertTrue(
stabilitySnapshot(tree) != null,
"non-route attribute must not be counted as a screen",
)
} }
@Test fun countRouteScreensCountsTestTagAndIdentifier() { @Test fun countRouteScreensCountsTestTagAndIdentifier() {
@@ -12,14 +12,15 @@ import kotlin.test.assertTrue
class WdaRecoveryTest { class WdaRecoveryTest {
private fun recovery( private fun recovery(isAlive: () -> Boolean, restart: () -> Unit) =
isAlive: () -> Boolean, WdaRecovery(isAlive = isAlive, restart = restart, log = {})
restart: () -> Unit,
) = WdaRecovery(isAlive = isAlive, restart = restart, log = {})
@Test fun aliveChannelSkipsRestartAndRetriesReads() { @Test fun aliveChannelSkipsRestartAndRetriesReads() {
val restarts = AtomicInteger(0) val restarts = AtomicInteger(0)
val recovery = recovery(isAlive = { true }, restart = { restarts.incrementAndGet() }) val recovery = recovery(
isAlive = { true },
restart = { restarts.incrementAndGet() },
)
var calls = 0 var calls = 0
val result = recovery.run(replay = true) { val result = recovery.run(replay = true) {
@@ -35,10 +36,15 @@ class WdaRecoveryTest {
@Test fun aliveChannelSurfacesUnavailableForActions() { @Test fun aliveChannelSurfacesUnavailableForActions() {
val restarts = AtomicInteger(0) val restarts = AtomicInteger(0)
val recovery = recovery(isAlive = { true }, restart = { restarts.incrementAndGet() }) val recovery = recovery(
isAlive = { true },
restart = { restarts.incrementAndGet() },
)
val thrown = assertFailsWith<io.grpc.StatusRuntimeException> { val thrown = assertFailsWith<io.grpc.StatusRuntimeException> {
recovery.run(replay = false) { throw IOException("connection reset") } recovery.run(replay = false) {
throw IOException("connection reset")
}
} }
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code) assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
@@ -101,7 +107,9 @@ class WdaRecoveryTest {
) )
val thrown = assertFailsWith<IllegalStateException> { val thrown = assertFailsWith<IllegalStateException> {
recovery.run(replay = true) { throw IOException("connection refused") } recovery.run(replay = true) {
throw IOException("connection refused")
}
} }
assertTrue(thrown.message.orEmpty().contains("WDA reconnect failed")) assertTrue(thrown.message.orEmpty().contains("WDA reconnect failed"))
@@ -116,7 +124,9 @@ class WdaRecoveryTest {
) )
assertFailsWith<IllegalArgumentException> { assertFailsWith<IllegalArgumentException> {
recovery.run(replay = true) { throw IllegalArgumentException("bad selector") } recovery.run(replay = true) {
throw IllegalArgumentException("bad selector")
}
} }
assertEquals(0, restarts.get()) assertEquals(0, restarts.get())
@@ -127,7 +137,9 @@ class WdaRecoveryTest {
val recovery = recovery(isAlive = { true }, restart = {}) val recovery = recovery(isAlive = { true }, restart = {})
val thrown = assertFailsWith<io.grpc.StatusRuntimeException> { val thrown = assertFailsWith<io.grpc.StatusRuntimeException> {
recovery.run(replay = true) { throw IOException("connection reset") } recovery.run(replay = true) {
throw IOException("connection reset")
}
} }
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code) assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)