mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
Merge branch 'ci-hygiene-and-fmt' into trial-merge
This commit is contained in:
commit
4caafafb1d
20 files changed
+541
-204
No files matched your search
@@ -189,7 +189,12 @@ if [ "$platform" = "android" ]; then
|
||||
*) echo "folio/android: the harness failed with exit $code" >&2; exit "$code" ;;
|
||||
esac
|
||||
if ! grep -q '"AddTransactionScreen"' "$trace"; then
|
||||
echo "folio/android: the run never reached AddTransactionScreen, so it never got past login" >&2
|
||||
# Where it stopped is a much longer question than this gate answers, so
|
||||
# name the routes the trace holds and leave the diagnosis to the reader.
|
||||
reached=$(grep -oE '"[A-Za-z0-9]+Screen"' "$trace" | tr -d '"' |
|
||||
awk '!seen[$0]++' | paste -sd, -) || reached=""
|
||||
echo "folio/android: the run never reached AddTransactionScreen over $steps steps" >&2
|
||||
echo "folio/android: routes the trace does record: ${reached:-none}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "folio/android: healthy run over $steps steps, reached the transaction screen"
|
||||
|
||||
+12
-13
@@ -19,25 +19,25 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
uses: android-actions/setup-android@v4
|
||||
|
||||
- name: Set up Node 22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Cache bun store
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-${{ runner.os }}-${{ hashFiles('replay-ui/bun.lock') }}
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -98,19 +98,18 @@ jobs:
|
||||
browser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
# Pin stable: the action's default (latest) pulls a dev Chromium whose
|
||||
# remote-debugging socket is flaky under the driver, even though the
|
||||
# browser otherwise launches headless.
|
||||
# Pin stable: a dev Chromium's remote-debugging socket is flaky under
|
||||
# the driver, even though the browser otherwise launches headless.
|
||||
- name: Set up Chrome
|
||||
uses: browser-actions/setup-chrome@v1
|
||||
uses: browser-actions/setup-chrome@v2
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install pandoc
|
||||
run: sudo apt-get update && sudo apt-get install -y pandoc
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Build site
|
||||
run: make docs
|
||||
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
- uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: build/site
|
||||
|
||||
@@ -41,5 +41,5 @@ jobs:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/deploy-pages@v4
|
||||
- uses: actions/deploy-pages@v5
|
||||
id: deployment
|
||||
+18
-18
@@ -41,16 +41,16 @@ jobs:
|
||||
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'android' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Set up the JDKs
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
# The metro gradle plugin folio builds with needs a 21 runtime; the
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
21
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
uses: android-actions/setup-android@v4
|
||||
|
||||
- name: Set up bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
|
||||
- name: Upload the run
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: folio-android
|
||||
path: runs/
|
||||
@@ -120,10 +120,10 @@ jobs:
|
||||
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'ios' }}
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
run: brew install facebook/fb/idb-companion xcodegen just
|
||||
|
||||
- name: Set up the JDKs
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
# The metro gradle plugin folio builds with needs a 21 runtime; the
|
||||
@@ -152,13 +152,13 @@ jobs:
|
||||
# project, which configures :app:androidApp and so needs an Android SDK
|
||||
# even on this leg.
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
uses: android-actions/setup-android@v4
|
||||
|
||||
# Both asset tarballs are built by the prepare scripts, and the runner
|
||||
# bundle is an xcodebuild of companion/Sources. Keyed on the scripts and
|
||||
# the versions the Makefile embeds, so a later run reuses them.
|
||||
- name: Cache the companion and runner bundles
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
internal/driver/ioscompanion/companionassets/assets
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
|
||||
- name: Upload the run
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: folio-ios
|
||||
path: runs/
|
||||
@@ -207,16 +207,16 @@ jobs:
|
||||
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'web' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Set up the JDKs
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
# The metro gradle plugin folio builds with needs a 21 runtime; the
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -241,7 +241,7 @@ jobs:
|
||||
folio-gradle-${{ runner.os }}-
|
||||
|
||||
- name: Set up Chrome
|
||||
uses: browser-actions/setup-chrome@v1
|
||||
uses: browser-actions/setup-chrome@v2
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
@@ -270,7 +270,7 @@ jobs:
|
||||
|
||||
- name: Upload the run
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: folio-web
|
||||
path: runs/
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
echo "version=${raw#v}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node 22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "22"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
@@ -65,28 +65,28 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
uses: android-actions/setup-android@v4
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
run: make sidecar
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: "~> v2"
|
||||
args: release --clean
|
||||
|
||||
@@ -27,10 +27,10 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
# Pinned stable plus the AppArmor sysctl: the same setup ci.yml's browser
|
||||
# job needs to get headless Chrome up on ubuntu-latest.
|
||||
- name: Set up Chrome
|
||||
uses: browser-actions/setup-chrome@v1
|
||||
uses: browser-actions/setup-chrome@v2
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
- name: Upload runs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: replay-ui-runs
|
||||
path: runs/
|
||||
|
||||
@@ -37,7 +37,9 @@ IOS_DEVICE="iPhone 15" just ios # pick a different simulator
|
||||
|
||||
`just ios` regenerates `app/iosApp/iosApp.xcodeproj` from `app/iosApp/project.yml`,
|
||||
builds the KMP framework (`Shared.framework` from `:app:shared`), links it
|
||||
into the SwiftUI host, installs, and launches.
|
||||
into the SwiftUI host, uninstalls any previous copy, installs, and launches.
|
||||
The uninstall matters: folio's signed-in session survives an install over the
|
||||
top, so without it a run opens on the last run's Home screen.
|
||||
|
||||
## Web
|
||||
|
||||
|
||||
@@ -130,6 +130,10 @@ ios:
|
||||
-destination 'platform=iOS Simulator,name={{ios_device}}' \
|
||||
-derivedDataPath app/iosApp/build \
|
||||
build | tail -5
|
||||
# Installing over the top keeps the data container, and folio's signed-in
|
||||
# session with it, so a run started straight after would open on the last
|
||||
# run's Home screen instead of Login and diverge at step 1.
|
||||
xcrun simctl uninstall booted app.folio || true
|
||||
xcrun simctl install booted "{{ios_app}}"
|
||||
xcrun simctl launch booted app.folio
|
||||
|
||||
|
||||
@@ -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