mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
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:
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 snapshotLock = Any()
|
||||
|
||||
override fun launch(request: LaunchRequest, responseObserver: StreamObserver<Empty>) {
|
||||
override fun launch(
|
||||
request: LaunchRequest,
|
||||
responseObserver: StreamObserver<Empty>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
backend.launch(request.bundleId, request.clearState, request.envMap)
|
||||
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) {
|
||||
launchedBundleId.get()?.let { backend.terminate(it) }
|
||||
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) {
|
||||
backend.doubleTap(request.x, request.y)
|
||||
Empty.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun longPress(request: Point, responseObserver: StreamObserver<Empty>) {
|
||||
override fun longPress(
|
||||
request: Point,
|
||||
responseObserver: StreamObserver<Empty>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
backend.longPress(request.x, request.y)
|
||||
Empty.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun tapSelector(request: Selector, responseObserver: StreamObserver<Empty>) {
|
||||
override fun tapSelector(
|
||||
request: Selector,
|
||||
responseObserver: StreamObserver<Empty>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
backend.tapSelector(request.value)
|
||||
Empty.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun inputText(request: Text, responseObserver: StreamObserver<Empty>) {
|
||||
override fun inputText(
|
||||
request: Text,
|
||||
responseObserver: StreamObserver<Empty>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
backend.inputText(request.value)
|
||||
Empty.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun eraseText(request: EraseTextRequest, responseObserver: StreamObserver<Empty>) {
|
||||
override fun eraseText(
|
||||
request: EraseTextRequest,
|
||||
responseObserver: StreamObserver<Empty>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
backend.eraseText(request.characterCount)
|
||||
Empty.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun swipe(request: SwipeRequest, responseObserver: StreamObserver<Empty>) {
|
||||
override fun swipe(
|
||||
request: SwipeRequest,
|
||||
responseObserver: StreamObserver<Empty>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
val from = request.from
|
||||
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) {
|
||||
backend.pressKey(request.key)
|
||||
Empty.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun recentLogs(request: RecentLogsRequest, responseObserver: StreamObserver<LogEntries>) {
|
||||
override fun recentLogs(
|
||||
request: RecentLogsRequest,
|
||||
responseObserver: StreamObserver<LogEntries>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
val entries = backend.recentLogs(request.sinceUnixMillis, request.levelAtLeast)
|
||||
val entries = backend.recentLogs(
|
||||
request.sinceUnixMillis,
|
||||
request.levelAtLeast,
|
||||
)
|
||||
val builder = LogEntries.newBuilder()
|
||||
for (entry in entries) {
|
||||
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) {
|
||||
val (png, width, height) = backend.screenshot()
|
||||
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) {
|
||||
HierarchyJSON.newBuilder().setJson(backend.hierarchy()).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun snapshot(request: Empty, responseObserver: StreamObserver<SnapshotResponse>) {
|
||||
override fun snapshot(
|
||||
request: Empty,
|
||||
responseObserver: StreamObserver<SnapshotResponse>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
val sample = synchronized(snapshotLock) { backend.snapshot() }
|
||||
val (png, width, height) = sample.screenshot
|
||||
SnapshotResponse.newBuilder()
|
||||
.setHierarchy(HierarchyJSON.newBuilder().setJson(sample.hierarchyJson).build())
|
||||
.setHierarchy(
|
||||
HierarchyJSON.newBuilder()
|
||||
.setJson(sample.hierarchyJson)
|
||||
.build(),
|
||||
)
|
||||
.setScreenshot(
|
||||
Image.newBuilder()
|
||||
.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) {
|
||||
backend.waitForIdle(request.millis)
|
||||
Empty.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun health(request: Empty, responseObserver: StreamObserver<HealthStatus>) {
|
||||
override fun health(
|
||||
request: Empty,
|
||||
responseObserver: StreamObserver<HealthStatus>,
|
||||
) {
|
||||
runRpc(responseObserver) {
|
||||
HealthStatus.newBuilder()
|
||||
.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) {
|
||||
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)
|
||||
MetricsResponse.newBuilder()
|
||||
.setCpuPercent(sample.cpuPercent)
|
||||
@@ -191,7 +250,9 @@ class DriverService(
|
||||
// stale session, then closes the backend so the iOS XCTest runner process
|
||||
// dies with us instead of being orphaned.
|
||||
fun shutdown() {
|
||||
runCatching { launchedBundleId.getAndSet(null)?.let { backend.terminate(it) } }
|
||||
runCatching {
|
||||
launchedBundleId.getAndSet(null)?.let { backend.terminate(it) }
|
||||
}
|
||||
runCatching { backend.close() }
|
||||
}
|
||||
|
||||
@@ -209,8 +270,10 @@ class DriverService(
|
||||
// failures that do not extend Exception, and an uncaught one
|
||||
// kills the RPC as a channel-level Unknown instead of a status
|
||||
// the runner can classify.
|
||||
observer.onError(io.grpc.Status.INTERNAL.withDescription(cause.toString())
|
||||
.withCause(cause).asRuntimeException())
|
||||
observer.onError(
|
||||
io.grpc.Status.INTERNAL.withDescription(cause.toString())
|
||||
.withCause(cause).asRuntimeException(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,17 @@ class SidecarServer(
|
||||
private val shutdownLatch = CountDownLatch(1)
|
||||
|
||||
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)
|
||||
.build()
|
||||
server.start()
|
||||
grpcServer = server
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
stop()
|
||||
})
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
Thread {
|
||||
stop()
|
||||
},
|
||||
)
|
||||
return server.port
|
||||
}
|
||||
|
||||
@@ -48,23 +51,41 @@ class SidecarServer(
|
||||
// lost from run output.
|
||||
private fun quietExpectedDriverNoise() {
|
||||
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(
|
||||
"xcuitest.XCTestDriverClient", org.apache.logging.log4j.Level.OFF)
|
||||
"xcuitest.XCTestDriverClient",
|
||||
org.apache.logging.log4j.Level.OFF,
|
||||
)
|
||||
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>) {
|
||||
quietExpectedDriverNoise()
|
||||
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 ->
|
||||
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 ->
|
||||
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) {
|
||||
@@ -74,7 +95,9 @@ fun main(arguments: Array<String>) {
|
||||
val service = DriverService(platform = platform, backend = backend)
|
||||
val server = SidecarServer(port, service)
|
||||
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()
|
||||
server.awaitTermination()
|
||||
}
|
||||
@@ -10,7 +10,10 @@ class DadbTargetTest {
|
||||
}
|
||||
|
||||
@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() {
|
||||
@@ -20,6 +23,9 @@ class DadbTargetTest {
|
||||
// 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.
|
||||
@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)
|
||||
|
||||
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(56, cal.get(java.util.Calendar.SECOND))
|
||||
assertEquals(789, cal.get(java.util.Calendar.MILLISECOND))
|
||||
@@ -51,7 +52,9 @@ class DeviceOutputParserTest {
|
||||
|
||||
@Test fun parseCpuTicksReturnsNullOnTruncatedOrNonNumericStat() {
|
||||
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(""))
|
||||
}
|
||||
|
||||
@@ -79,8 +82,14 @@ class DeviceOutputParserTest {
|
||||
}
|
||||
|
||||
@Test fun parseBoundsAcceptsWellFormedAndRejectsMalformed() {
|
||||
assertEquals(listOf(0, 0, 1080, 2340), parseBounds("[0,0,1080,2340]")?.toList())
|
||||
assertEquals(listOf(-5, -10, 20, 30), parseBounds("[-5,-10,20,30]")?.toList())
|
||||
assertEquals(
|
||||
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,1,1"))
|
||||
assertNull(parseBounds("[0, 0, 1, 1]"))
|
||||
@@ -92,10 +101,14 @@ class DeviceOutputParserTest {
|
||||
"resource-id" to "com.example:id/loginButton",
|
||||
"bounds" to "[10,20,110,80]",
|
||||
)
|
||||
assertEquals(listOf(10, 20, 110, 80), findBoundsBySelector(tree, "id:loginButton")?.toList())
|
||||
assertEquals(
|
||||
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",
|
||||
children = listOf(
|
||||
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(listOf(5, 6, 7, 8), findBoundsBySelector(tree, "descPrefix:AccountCard")?.toList())
|
||||
assertEquals(
|
||||
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() {
|
||||
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:missing"))
|
||||
}
|
||||
|
||||
@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"))
|
||||
}
|
||||
|
||||
@@ -132,17 +160,26 @@ class DeviceOutputParserTest {
|
||||
private fun ihdr(width: Int, height: Int): ByteArray {
|
||||
val b = ByteArray(33)
|
||||
for (i in 0 until 8) b[8 + i] = 0
|
||||
b[12] = 'I'.code.toByte(); b[13] = 'H'.code.toByte()
|
||||
b[14] = 'D'.code.toByte(); b[15] = 'R'.code.toByte()
|
||||
b[16] = (width ushr 24).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()
|
||||
b[12] = 'I'.code.toByte()
|
||||
b[13] = 'H'.code.toByte()
|
||||
b[14] = 'D'.code.toByte()
|
||||
b[15] = 'R'.code.toByte()
|
||||
b[16] = (width ushr 24).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
|
||||
}
|
||||
|
||||
private fun node(
|
||||
vararg attrs: Pair<String, String>,
|
||||
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.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 {
|
||||
|
||||
@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 service = DriverService(platform = "android", backend = backend)
|
||||
grpcCleanup.register(
|
||||
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start()
|
||||
InProcessServerBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.addService(service)
|
||||
.build()
|
||||
.start(),
|
||||
)
|
||||
val channel: ManagedChannel = grpcCleanup.register(
|
||||
InProcessChannelBuilder.forName(serverName).directExecutor().build()
|
||||
InProcessChannelBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.build(),
|
||||
)
|
||||
return DriverGrpc.newBlockingStub(channel)
|
||||
}
|
||||
@@ -59,20 +73,32 @@ class DriverServiceTest {
|
||||
var terminated: String? = null
|
||||
var closed = false
|
||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||
override fun terminate(bundleId: String) { terminated = bundleId }
|
||||
override fun close() { closed = true }
|
||||
override fun terminate(bundleId: String) {
|
||||
terminated = bundleId
|
||||
}
|
||||
override fun close() {
|
||||
closed = true
|
||||
}
|
||||
}
|
||||
val serverName = InProcessServerBuilder.generateName()
|
||||
val service = DriverService(platform = "android", backend = backend)
|
||||
grpcCleanup.register(
|
||||
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start()
|
||||
InProcessServerBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.addService(service)
|
||||
.build()
|
||||
.start(),
|
||||
)
|
||||
val channel: ManagedChannel = grpcCleanup.register(
|
||||
InProcessChannelBuilder.forName(serverName).directExecutor().build()
|
||||
InProcessChannelBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.build(),
|
||||
)
|
||||
val client = DriverGrpc.newBlockingStub(channel)
|
||||
|
||||
client.launch(LaunchRequest.newBuilder().setBundleId("com.example").build())
|
||||
client.launch(
|
||||
LaunchRequest.newBuilder().setBundleId("com.example").build(),
|
||||
)
|
||||
service.shutdown()
|
||||
|
||||
assertEquals("com.example", terminated)
|
||||
@@ -83,8 +109,12 @@ class DriverServiceTest {
|
||||
var terminated: String? = null
|
||||
var closed = false
|
||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||
override fun terminate(bundleId: String) { terminated = bundleId }
|
||||
override fun close() { closed = true }
|
||||
override fun terminate(bundleId: String) {
|
||||
terminated = bundleId
|
||||
}
|
||||
override fun close() {
|
||||
closed = true
|
||||
}
|
||||
}
|
||||
val service = DriverService(platform = "android", backend = backend)
|
||||
|
||||
@@ -128,17 +158,17 @@ class DriverServiceTest {
|
||||
// the runner can tell transient failures from fatal ones.
|
||||
@Test fun backendStatusCodePassesThrough() {
|
||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||
override fun inputText(text: String) {
|
||||
override fun inputText(text: String): Unit =
|
||||
throw io.grpc.Status.UNAVAILABLE
|
||||
.withDescription("connection dropped mid-action")
|
||||
.asRuntimeException()
|
||||
}
|
||||
}
|
||||
val client = newClient(backend)
|
||||
|
||||
val thrown = kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
|
||||
client.inputText(Text.newBuilder().setValue("hello").build())
|
||||
}
|
||||
val thrown =
|
||||
kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
|
||||
client.inputText(Text.newBuilder().setValue("hello").build())
|
||||
}
|
||||
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
|
||||
}
|
||||
|
||||
@@ -147,17 +177,19 @@ class DriverServiceTest {
|
||||
// channel-level Unknown the runner cannot classify.
|
||||
@Test fun nonExceptionThrowableMapsToInternal() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
val client = newClient(backend)
|
||||
|
||||
val thrown = kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
|
||||
client.inputText(Text.newBuilder().setValue("hello").build())
|
||||
}
|
||||
val thrown =
|
||||
kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
|
||||
client.inputText(Text.newBuilder().setValue("hello").build())
|
||||
}
|
||||
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() {
|
||||
@@ -171,7 +203,16 @@ class DriverServiceTest {
|
||||
assertEquals("pkill", commands[0][0])
|
||||
assertTrue(commands[0][2].contains("test-without-building"))
|
||||
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() {
|
||||
@@ -192,7 +233,9 @@ class DriverServiceTest {
|
||||
// still executing fails instead of queuing.
|
||||
val tapAction = {
|
||||
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()
|
||||
Thread.sleep(150)
|
||||
@@ -211,7 +254,9 @@ class DriverServiceTest {
|
||||
val tapAction = {
|
||||
if (failedFirst.compareAndSet(false, true)) {
|
||||
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()
|
||||
Unit
|
||||
@@ -226,21 +271,38 @@ class DriverServiceTest {
|
||||
// directly.
|
||||
val taps = mutableListOf<Pair<Int, Int>>()
|
||||
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 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 inputText(text: String) {}
|
||||
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 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 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 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)
|
||||
|
||||
@@ -252,13 +314,16 @@ class DriverServiceTest {
|
||||
val backend = StubDriverBackend("android")
|
||||
val client = newClient(backend)
|
||||
|
||||
client.eraseText(EraseTextRequest.newBuilder().setCharacterCount(11).build())
|
||||
client.eraseText(
|
||||
EraseTextRequest.newBuilder().setCharacterCount(11).build(),
|
||||
)
|
||||
assertEquals(11, backend.lastEraseCharacterCount)
|
||||
}
|
||||
|
||||
@Test fun screenshotReturnsBackendBytes() {
|
||||
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)
|
||||
|
||||
@@ -294,7 +359,13 @@ class DriverServiceTest {
|
||||
@Test fun swipeForwardsEndpointsAndDuration() {
|
||||
var observed: Quintuple<Int, Int, Int, Int, Long>? = null
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -325,14 +396,18 @@ class DriverServiceTest {
|
||||
|
||||
@Test fun recentLogsReturnsBackendEntries() {
|
||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> {
|
||||
return listOf(LogLine(1, "E", "AndroidRuntime", "boom"))
|
||||
}
|
||||
override fun recentLogs(
|
||||
sinceUnixMillis: Long,
|
||||
minLevel: String,
|
||||
): List<LogLine> = listOf(LogLine(1, "E", "AndroidRuntime", "boom"))
|
||||
}
|
||||
val client = newClient(backend)
|
||||
|
||||
val response = client.recentLogs(
|
||||
RecentLogsRequest.newBuilder().setSinceUnixMillis(0).setLevelAtLeast("E").build(),
|
||||
RecentLogsRequest.newBuilder()
|
||||
.setSinceUnixMillis(0)
|
||||
.setLevelAtLeast("E")
|
||||
.build(),
|
||||
)
|
||||
assertEquals(1, response.entriesCount)
|
||||
assertEquals("AndroidRuntime", response.getEntries(0).tag)
|
||||
@@ -351,7 +426,9 @@ class DriverServiceTest {
|
||||
}
|
||||
val client = newClient(backend)
|
||||
|
||||
client.launch(LaunchRequest.newBuilder().setBundleId("com.launched").build())
|
||||
client.launch(
|
||||
LaunchRequest.newBuilder().setBundleId("com.launched").build(),
|
||||
)
|
||||
client.metrics(MetricsRequest.getDefaultInstance())
|
||||
|
||||
assertEquals("com.launched", sampled)
|
||||
@@ -367,8 +444,12 @@ class DriverServiceTest {
|
||||
}
|
||||
val client = newClient(backend)
|
||||
|
||||
client.launch(LaunchRequest.newBuilder().setBundleId("com.launched").build())
|
||||
client.metrics(MetricsRequest.newBuilder().setBundleId("com.other").build())
|
||||
client.launch(
|
||||
LaunchRequest.newBuilder().setBundleId("com.launched").build(),
|
||||
)
|
||||
client.metrics(
|
||||
MetricsRequest.newBuilder().setBundleId("com.other").build(),
|
||||
)
|
||||
|
||||
assertEquals("com.other", sampled)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ class InputTextTest {
|
||||
val fallback = listOf(
|
||||
"Emergency Fund", "🙂🔥💸", " ", "\t\n", "'; DROP TABLE--",
|
||||
"<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) {
|
||||
assertTrue(
|
||||
@@ -190,12 +191,12 @@ class InputTextTest {
|
||||
|
||||
@Test fun parseResumedPackageReadsEachResumedActivityWording() {
|
||||
val cases = mapOf(
|
||||
" topResumedActivity=ActivityRecord{8b u0 app.folio/.MainActivity t42}" to
|
||||
"app.folio",
|
||||
" mResumedActivity: ActivityRecord{1c u0 com.example.app/.Home t9}" to
|
||||
"com.example.app",
|
||||
" ResumedActivity: ActivityRecord{2d u0 app.folio/com.folio.Detail t9}" to
|
||||
"app.folio",
|
||||
" topResumedActivity=ActivityRecord{8b u0 " +
|
||||
"app.folio/.MainActivity t42}" to "app.folio",
|
||||
" mResumedActivity: ActivityRecord{1c u0 " +
|
||||
"com.example.app/.Home t9}" to "com.example.app",
|
||||
" ResumedActivity: ActivityRecord{2d u0 " +
|
||||
"app.folio/com.folio.Detail t9}" to "app.folio",
|
||||
)
|
||||
for ((line, want) in cases) {
|
||||
assertEquals(want, parseResumedPackage(line), line)
|
||||
|
||||
@@ -12,28 +12,40 @@ class ResolveActivityTest {
|
||||
com.example.app/.MainActivity
|
||||
""".trimIndent()
|
||||
|
||||
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output)
|
||||
val activity = StubDriverBackend.parseResolvedActivity(
|
||||
"com.example.app",
|
||||
output,
|
||||
)
|
||||
assertEquals(".MainActivity", activity)
|
||||
}
|
||||
|
||||
@Test fun extractsFullyQualifiedActivity() {
|
||||
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)
|
||||
}
|
||||
|
||||
@Test fun returnsNullWhenPackageNotFound() {
|
||||
val output = "No activity found"
|
||||
|
||||
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output)
|
||||
val activity = StubDriverBackend.parseResolvedActivity(
|
||||
"com.example.app",
|
||||
output,
|
||||
)
|
||||
assertNull(activity)
|
||||
}
|
||||
|
||||
@Test fun doesNotMatchDifferentPackagePrefix() {
|
||||
val output = "other.pkg/.MainActivity"
|
||||
|
||||
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output)
|
||||
val activity = StubDriverBackend.parseResolvedActivity(
|
||||
"com.example.app",
|
||||
output,
|
||||
)
|
||||
assertNull(activity)
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,13 @@ class RouteTransitionTest {
|
||||
private fun screen(id: String, child: String = "") =
|
||||
"""{"attributes":{"resource-id":"$id"},"children":[$child]}"""
|
||||
|
||||
private fun tree(vararg children: String) =
|
||||
"""{"attributes":{"resource-id":"root"},"children":[${children.joinToString(",")}]}"""
|
||||
private fun tree(vararg children: String): String {
|
||||
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"))
|
||||
|
||||
@Test fun waitsForTheCrossFadeToLandAndReturnsTheLandedTree() {
|
||||
@@ -29,8 +32,15 @@ class RouteTransitionTest {
|
||||
reads++
|
||||
if (reads <= 3) crossFade else landed
|
||||
}
|
||||
assertTrue(reads > 3, "must keep reading until the fade lands, reads=$reads")
|
||||
assertEquals(1, countRouteScreens(settled), "must return a tree with one route")
|
||||
assertTrue(
|
||||
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() {
|
||||
@@ -54,13 +64,21 @@ class RouteTransitionTest {
|
||||
// would burn the whole poll budget and still hand over a frame the
|
||||
// runner refuses to act on.
|
||||
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
|
||||
awaitSettledTree {
|
||||
reads++
|
||||
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() {
|
||||
@@ -78,7 +96,11 @@ class RouteTransitionTest {
|
||||
elapsed < TRANSITION_POLL_CAP_MILLIS + 1000L,
|
||||
"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() {
|
||||
@@ -89,20 +111,29 @@ class RouteTransitionTest {
|
||||
val fadeMillis = 700L
|
||||
val start = System.currentTimeMillis()
|
||||
val settled = awaitSettledTree {
|
||||
if (System.currentTimeMillis() - start < fadeMillis) crossFade else landed
|
||||
if (System.currentTimeMillis() - start < fadeMillis) {
|
||||
crossFade
|
||||
} else {
|
||||
landed
|
||||
}
|
||||
}
|
||||
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(
|
||||
elapsed >= fadeMillis,
|
||||
"cannot have settled before the fade ended, elapsed=${elapsed}ms",
|
||||
)
|
||||
assertTrue(
|
||||
elapsed < TRANSITION_POLL_CAP_MILLIS,
|
||||
"the ${TRANSITION_POLL_CAP_MILLIS}ms cap has to leave room for a ${fadeMillis}ms " +
|
||||
"fade and the ${TRANSITION_STABLE_STREAK_MILLIS}ms streak after it, but the " +
|
||||
"wait ran to the cap instead, elapsed=${elapsed}ms",
|
||||
"the ${TRANSITION_POLL_CAP_MILLIS}ms cap has to leave room for " +
|
||||
"a ${fadeMillis}ms fade and the " +
|
||||
"${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 {
|
||||
@Test
|
||||
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()
|
||||
try {
|
||||
assertTrue(boundPort > 0, "expected ephemeral port, got $boundPort")
|
||||
|
||||
@@ -19,14 +19,22 @@ class SnapshotHandlerTest {
|
||||
|
||||
@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 service = DriverService(platform = "android", backend = backend)
|
||||
grpcCleanup.register(
|
||||
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start(),
|
||||
InProcessServerBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.addService(service)
|
||||
.build()
|
||||
.start(),
|
||||
)
|
||||
val channel: ManagedChannel = grpcCleanup.register(
|
||||
InProcessChannelBuilder.forName(serverName).directExecutor().build(),
|
||||
InProcessChannelBuilder.forName(serverName)
|
||||
.directExecutor()
|
||||
.build(),
|
||||
)
|
||||
return DriverGrpc.newBlockingStub(channel)
|
||||
}
|
||||
@@ -37,8 +45,10 @@ class SnapshotHandlerTest {
|
||||
// forward those calls to the delegate, not these overrides. Override
|
||||
// snapshot() directly so the test exercises the wire path end-to-end.
|
||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||
override fun snapshot(): SnapshotSample =
|
||||
SnapshotSample("{\"x\":1}", Triple(byteArrayOf(7, 8, 9), 1080, 2340))
|
||||
override fun snapshot(): SnapshotSample = SnapshotSample(
|
||||
"{\"x\":1}",
|
||||
Triple(byteArrayOf(7, 8, 9), 1080, 2340),
|
||||
)
|
||||
}
|
||||
val client = newClient(backend)
|
||||
|
||||
@@ -55,13 +65,23 @@ class SnapshotHandlerTest {
|
||||
// aligned with the final hierarchy snapshot the runner accepts.
|
||||
val callOrder = mutableListOf<String>()
|
||||
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 tap(x: Int, y: Int) {}
|
||||
override fun tapSelector(selector: String) {}
|
||||
override fun inputText(text: String) {}
|
||||
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 longPress(x: Int, y: Int) {}
|
||||
override fun screenshot(): Triple<ByteArray, Int, Int> {
|
||||
@@ -72,10 +92,14 @@ class SnapshotHandlerTest {
|
||||
callOrder.add("hierarchy")
|
||||
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 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()
|
||||
assertEquals(listOf("hierarchy", "screenshot"), callOrder)
|
||||
@@ -88,7 +112,8 @@ class SnapshotHandlerTest {
|
||||
val maxObserved = AtomicInteger(0)
|
||||
val callCount = AtomicInteger(0)
|
||||
val lock = ReentrantLock()
|
||||
val recordingBackend = object : DriverBackend by StubDriverBackend("android") {
|
||||
val delegate = StubDriverBackend("android")
|
||||
val recordingBackend = object : DriverBackend by delegate {
|
||||
override fun snapshot(): SnapshotSample {
|
||||
val now = inFlight.incrementAndGet()
|
||||
try {
|
||||
@@ -109,9 +134,13 @@ class SnapshotHandlerTest {
|
||||
// Use a real (multi-threaded) executor on the server side so the service
|
||||
// is not artificially serialized by directExecutor.
|
||||
val serverName = InProcessServerBuilder.generateName()
|
||||
val service = DriverService(platform = "android", backend = recordingBackend)
|
||||
val service =
|
||||
DriverService(platform = "android", backend = recordingBackend)
|
||||
grpcCleanup.register(
|
||||
InProcessServerBuilder.forName(serverName).addService(service).build().start(),
|
||||
InProcessServerBuilder.forName(serverName)
|
||||
.addService(service)
|
||||
.build()
|
||||
.start(),
|
||||
)
|
||||
val channel: ManagedChannel = grpcCleanup.register(
|
||||
InProcessChannelBuilder.forName(serverName).build(),
|
||||
|
||||
@@ -13,7 +13,10 @@ class StabilityPollTest {
|
||||
elapsed >= MIN_STABLE_STREAK_MILLIS,
|
||||
"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() {
|
||||
@@ -38,8 +41,9 @@ class StabilityPollTest {
|
||||
val observedQuiet = sampleStarts.last() - sampleEnds.first()
|
||||
assertTrue(
|
||||
observedQuiet >= MIN_STABLE_STREAK_MILLIS,
|
||||
"the poll returned having observed only ${observedQuiet}ms of quiet, not " +
|
||||
"${MIN_STABLE_STREAK_MILLIS}ms; starts=$sampleStarts ends=$sampleEnds",
|
||||
"the poll returned having observed only ${observedQuiet}ms of " +
|
||||
"quiet, not ${MIN_STABLE_STREAK_MILLIS}ms; " +
|
||||
"starts=$sampleStarts ends=$sampleEnds",
|
||||
)
|
||||
assertTrue(
|
||||
sampleStarts.size >= 3,
|
||||
@@ -60,23 +64,27 @@ class StabilityPollTest {
|
||||
calls++
|
||||
when {
|
||||
calls <= 2 -> "calm"
|
||||
|
||||
calls == 3 -> {
|
||||
transientAt = System.currentTimeMillis()
|
||||
"transient"
|
||||
}
|
||||
|
||||
else -> "stable"
|
||||
}
|
||||
}
|
||||
val sinceTransition = System.currentTimeMillis() - transientAt
|
||||
assertTrue(
|
||||
calls >= 8,
|
||||
"after the transition the poll needs a fresh matching pair and then a full " +
|
||||
"${MIN_STABLE_STREAK_MILLIS}ms of quiet, which is 8 samples, got $calls",
|
||||
"after the transition the poll needs a fresh matching pair and " +
|
||||
"then a full ${MIN_STABLE_STREAK_MILLIS}ms of quiet, which " +
|
||||
"is 8 samples, got $calls",
|
||||
)
|
||||
assertTrue(
|
||||
sinceTransition >= MIN_STABLE_STREAK_MILLIS,
|
||||
"the calm prefix must not count: a full ${MIN_STABLE_STREAK_MILLIS}ms streak has to " +
|
||||
"start over after the transition, returned ${sinceTransition}ms after it",
|
||||
"the calm prefix must not count: a full " +
|
||||
"${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"
|
||||
}
|
||||
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() {
|
||||
@@ -117,7 +128,8 @@ class StabilityPollTest {
|
||||
assertEquals(0, calls)
|
||||
}
|
||||
|
||||
@Test fun structuralHashIgnoresBoundsAndIdenticalForSemanticallyEqualTrees() {
|
||||
@Test
|
||||
fun structuralHashIgnoresBoundsAndIdenticalForSemanticallyEqualTrees() {
|
||||
val a = """
|
||||
{"attributes":{"resource-id":"LoginScreen","bounds":"[0,0,1080,2340]"},
|
||||
"children":[
|
||||
@@ -130,13 +142,24 @@ class StabilityPollTest {
|
||||
{"attributes":{"resource-id":"LoginEmail","bounds":"[10,11,1070,101]","text":"a@b"},"children":[]}
|
||||
]}
|
||||
""".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() {
|
||||
val a = """{"attributes":{"resource-id":"LoginEmail","text":"a@b"},"children":[]}"""
|
||||
val b = """{"attributes":{"resource-id":"LoginEmail","text":"c@d"},"children":[]}"""
|
||||
assertTrue(structuralHash(a) != structuralHash(b), "text change must alter hash")
|
||||
val a = """
|
||||
{"attributes":{"resource-id":"LoginEmail","text":"a@b"},"children":[]}
|
||||
""".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() {
|
||||
@@ -160,7 +183,10 @@ class StabilityPollTest {
|
||||
]}
|
||||
""".trimIndent()
|
||||
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() {
|
||||
@@ -173,7 +199,10 @@ class StabilityPollTest {
|
||||
{"attributes":{"text":"Welcome to MyScreen"},"children":[]}
|
||||
]}
|
||||
""".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() {
|
||||
|
||||
@@ -12,14 +12,15 @@ import kotlin.test.assertTrue
|
||||
|
||||
class WdaRecoveryTest {
|
||||
|
||||
private fun recovery(
|
||||
isAlive: () -> Boolean,
|
||||
restart: () -> Unit,
|
||||
) = WdaRecovery(isAlive = isAlive, restart = restart, log = {})
|
||||
private fun recovery(isAlive: () -> Boolean, restart: () -> Unit) =
|
||||
WdaRecovery(isAlive = isAlive, restart = restart, log = {})
|
||||
|
||||
@Test fun aliveChannelSkipsRestartAndRetriesReads() {
|
||||
val restarts = AtomicInteger(0)
|
||||
val recovery = recovery(isAlive = { true }, restart = { restarts.incrementAndGet() })
|
||||
val recovery = recovery(
|
||||
isAlive = { true },
|
||||
restart = { restarts.incrementAndGet() },
|
||||
)
|
||||
var calls = 0
|
||||
|
||||
val result = recovery.run(replay = true) {
|
||||
@@ -35,10 +36,15 @@ class WdaRecoveryTest {
|
||||
|
||||
@Test fun aliveChannelSurfacesUnavailableForActions() {
|
||||
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> {
|
||||
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)
|
||||
@@ -101,7 +107,9 @@ class WdaRecoveryTest {
|
||||
)
|
||||
|
||||
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"))
|
||||
@@ -116,7 +124,9 @@ class WdaRecoveryTest {
|
||||
)
|
||||
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
recovery.run(replay = true) { throw IllegalArgumentException("bad selector") }
|
||||
recovery.run(replay = true) {
|
||||
throw IllegalArgumentException("bad selector")
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(0, restarts.get())
|
||||
@@ -127,7 +137,9 @@ class WdaRecoveryTest {
|
||||
val recovery = recovery(isAlive = { true }, restart = {})
|
||||
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user