mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 12:07:09 +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" ;;
|
*) echo "folio/android: the harness failed with exit $code" >&2; exit "$code" ;;
|
||||||
esac
|
esac
|
||||||
if ! grep -q '"AddTransactionScreen"' "$trace"; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "folio/android: healthy run over $steps steps, reached the transaction screen"
|
echo "folio/android: healthy run over $steps steps, reached the transaction screen"
|
||||||
|
|||||||
+12
-13
@@ -19,25 +19,25 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
|
|
||||||
- name: Set up Android SDK
|
- name: Set up Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v4
|
||||||
|
|
||||||
- name: Set up Node 22
|
- name: Set up Node 22
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
bun-version: "1.3.13"
|
bun-version: "1.3.13"
|
||||||
|
|
||||||
- name: Cache bun store
|
- name: Cache bun store
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: ~/.bun/install/cache
|
path: ~/.bun/install/cache
|
||||||
key: bun-${{ runner.os }}-${{ hashFiles('replay-ui/bun.lock') }}
|
key: bun-${{ runner.os }}-${{ hashFiles('replay-ui/bun.lock') }}
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -98,19 +98,18 @@ jobs:
|
|||||||
browser:
|
browser:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
# Pin stable: the action's default (latest) pulls a dev Chromium whose
|
# Pin stable: a dev Chromium's remote-debugging socket is flaky under
|
||||||
# remote-debugging socket is flaky under the driver, even though the
|
# the driver, even though the browser otherwise launches headless.
|
||||||
# browser otherwise launches headless.
|
|
||||||
- name: Set up Chrome
|
- name: Set up Chrome
|
||||||
uses: browser-actions/setup-chrome@v1
|
uses: browser-actions/setup-chrome@v2
|
||||||
with:
|
with:
|
||||||
chrome-version: stable
|
chrome-version: stable
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Install pandoc
|
- name: Install pandoc
|
||||||
run: sudo apt-get update && sudo apt-get install -y pandoc
|
run: sudo apt-get update && sudo apt-get install -y pandoc
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- name: Build site
|
- name: Build site
|
||||||
run: make docs
|
run: make docs
|
||||||
|
|
||||||
- uses: actions/upload-pages-artifact@v3
|
- uses: actions/upload-pages-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: build/site
|
path: build/site
|
||||||
|
|
||||||
@@ -41,5 +41,5 @@ jobs:
|
|||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/deploy-pages@v4
|
- uses: actions/deploy-pages@v5
|
||||||
id: deployment
|
id: deployment
|
||||||
+18
-18
@@ -41,16 +41,16 @@ jobs:
|
|||||||
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'android' }}
|
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'android' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Set up the JDKs
|
- name: Set up the JDKs
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
# The metro gradle plugin folio builds with needs a 21 runtime; the
|
# The metro gradle plugin folio builds with needs a 21 runtime; the
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
21
|
21
|
||||||
|
|
||||||
- name: Set up Android SDK
|
- name: Set up Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v4
|
||||||
|
|
||||||
- name: Set up bun
|
- name: Set up bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
bun-version: "1.3.13"
|
bun-version: "1.3.13"
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload the run
|
- name: Upload the run
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: folio-android
|
name: folio-android
|
||||||
path: runs/
|
path: runs/
|
||||||
@@ -120,10 +120,10 @@ jobs:
|
|||||||
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'ios' }}
|
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'ios' }}
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
@@ -139,7 +139,7 @@ jobs:
|
|||||||
run: brew install facebook/fb/idb-companion xcodegen just
|
run: brew install facebook/fb/idb-companion xcodegen just
|
||||||
|
|
||||||
- name: Set up the JDKs
|
- name: Set up the JDKs
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
# The metro gradle plugin folio builds with needs a 21 runtime; the
|
# 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
|
# project, which configures :app:androidApp and so needs an Android SDK
|
||||||
# even on this leg.
|
# even on this leg.
|
||||||
- name: Set up Android SDK
|
- 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
|
# Both asset tarballs are built by the prepare scripts, and the runner
|
||||||
# bundle is an xcodebuild of companion/Sources. Keyed on the scripts and
|
# bundle is an xcodebuild of companion/Sources. Keyed on the scripts and
|
||||||
# the versions the Makefile embeds, so a later run reuses them.
|
# the versions the Makefile embeds, so a later run reuses them.
|
||||||
- name: Cache the companion and runner bundles
|
- name: Cache the companion and runner bundles
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
internal/driver/ioscompanion/companionassets/assets
|
internal/driver/ioscompanion/companionassets/assets
|
||||||
@@ -196,7 +196,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload the run
|
- name: Upload the run
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: folio-ios
|
name: folio-ios
|
||||||
path: runs/
|
path: runs/
|
||||||
@@ -207,16 +207,16 @@ jobs:
|
|||||||
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'web' }}
|
if: ${{ inputs.platforms == 'all' || inputs.platforms == 'web' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Set up the JDKs
|
- name: Set up the JDKs
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
# The metro gradle plugin folio builds with needs a 21 runtime; the
|
# The metro gradle plugin folio builds with needs a 21 runtime; the
|
||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
bun-version: "1.3.13"
|
bun-version: "1.3.13"
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -241,7 +241,7 @@ jobs:
|
|||||||
folio-gradle-${{ runner.os }}-
|
folio-gradle-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Set up Chrome
|
- name: Set up Chrome
|
||||||
uses: browser-actions/setup-chrome@v1
|
uses: browser-actions/setup-chrome@v2
|
||||||
with:
|
with:
|
||||||
chrome-version: stable
|
chrome-version: stable
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload the run
|
- name: Upload the run
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: folio-web
|
name: folio-web
|
||||||
path: runs/
|
path: runs/
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag || github.ref }}
|
ref: ${{ inputs.tag || github.ref }}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
echo "version=${raw#v}" >> "$GITHUB_OUTPUT"
|
echo "version=${raw#v}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up Node 22
|
- name: Set up Node 22
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
@@ -65,28 +65,28 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag || github.ref }}
|
ref: ${{ inputs.tag || github.ref }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
|
|
||||||
- name: Set up Android SDK
|
- name: Set up Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v4
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
run: make sidecar
|
run: make sidecar
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
version: "~> v2"
|
version: "~> v2"
|
||||||
args: release --clean
|
args: release --clean
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ jobs:
|
|||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
# Pinned stable plus the AppArmor sysctl: the same setup ci.yml's browser
|
# Pinned stable plus the AppArmor sysctl: the same setup ci.yml's browser
|
||||||
# job needs to get headless Chrome up on ubuntu-latest.
|
# job needs to get headless Chrome up on ubuntu-latest.
|
||||||
- name: Set up Chrome
|
- name: Set up Chrome
|
||||||
uses: browser-actions/setup-chrome@v1
|
uses: browser-actions/setup-chrome@v2
|
||||||
with:
|
with:
|
||||||
chrome-version: stable
|
chrome-version: stable
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload runs
|
- name: Upload runs
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: replay-ui-runs
|
name: replay-ui-runs
|
||||||
path: 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`,
|
`just ios` regenerates `app/iosApp/iosApp.xcodeproj` from `app/iosApp/project.yml`,
|
||||||
builds the KMP framework (`Shared.framework` from `:app:shared`), links it
|
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
|
## Web
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ ios:
|
|||||||
-destination 'platform=iOS Simulator,name={{ios_device}}' \
|
-destination 'platform=iOS Simulator,name={{ios_device}}' \
|
||||||
-derivedDataPath app/iosApp/build \
|
-derivedDataPath app/iosApp/build \
|
||||||
build | tail -5
|
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 install booted "{{ios_app}}"
|
||||||
xcrun simctl launch booted app.folio
|
xcrun simctl launch booted app.folio
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ class DriverService(
|
|||||||
private val launchedBundleId = AtomicReference<String?>(null)
|
private val launchedBundleId = AtomicReference<String?>(null)
|
||||||
private val snapshotLock = Any()
|
private val snapshotLock = Any()
|
||||||
|
|
||||||
override fun launch(request: LaunchRequest, responseObserver: StreamObserver<Empty>) {
|
override fun launch(
|
||||||
|
request: LaunchRequest,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.launch(request.bundleId, request.clearState, request.envMap)
|
backend.launch(request.bundleId, request.clearState, request.envMap)
|
||||||
launchedBundleId.set(request.bundleId)
|
launchedBundleId.set(request.bundleId)
|
||||||
@@ -39,7 +42,10 @@ class DriverService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun terminate(request: Empty, responseObserver: StreamObserver<Empty>) {
|
override fun terminate(
|
||||||
|
request: Empty,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
launchedBundleId.get()?.let { backend.terminate(it) }
|
launchedBundleId.get()?.let { backend.terminate(it) }
|
||||||
launchedBundleId.set(null)
|
launchedBundleId.set(null)
|
||||||
@@ -54,42 +60,60 @@ class DriverService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun doubleTap(request: Point, responseObserver: StreamObserver<Empty>) {
|
override fun doubleTap(
|
||||||
|
request: Point,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.doubleTap(request.x, request.y)
|
backend.doubleTap(request.x, request.y)
|
||||||
Empty.getDefaultInstance()
|
Empty.getDefaultInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun longPress(request: Point, responseObserver: StreamObserver<Empty>) {
|
override fun longPress(
|
||||||
|
request: Point,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.longPress(request.x, request.y)
|
backend.longPress(request.x, request.y)
|
||||||
Empty.getDefaultInstance()
|
Empty.getDefaultInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun tapSelector(request: Selector, responseObserver: StreamObserver<Empty>) {
|
override fun tapSelector(
|
||||||
|
request: Selector,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.tapSelector(request.value)
|
backend.tapSelector(request.value)
|
||||||
Empty.getDefaultInstance()
|
Empty.getDefaultInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inputText(request: Text, responseObserver: StreamObserver<Empty>) {
|
override fun inputText(
|
||||||
|
request: Text,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.inputText(request.value)
|
backend.inputText(request.value)
|
||||||
Empty.getDefaultInstance()
|
Empty.getDefaultInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun eraseText(request: EraseTextRequest, responseObserver: StreamObserver<Empty>) {
|
override fun eraseText(
|
||||||
|
request: EraseTextRequest,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.eraseText(request.characterCount)
|
backend.eraseText(request.characterCount)
|
||||||
Empty.getDefaultInstance()
|
Empty.getDefaultInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun swipe(request: SwipeRequest, responseObserver: StreamObserver<Empty>) {
|
override fun swipe(
|
||||||
|
request: SwipeRequest,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
val from = request.from
|
val from = request.from
|
||||||
val to = request.to
|
val to = request.to
|
||||||
@@ -98,16 +122,25 @@ class DriverService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pressKey(request: PressKeyRequest, responseObserver: StreamObserver<Empty>) {
|
override fun pressKey(
|
||||||
|
request: PressKeyRequest,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.pressKey(request.key)
|
backend.pressKey(request.key)
|
||||||
Empty.getDefaultInstance()
|
Empty.getDefaultInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recentLogs(request: RecentLogsRequest, responseObserver: StreamObserver<LogEntries>) {
|
override fun recentLogs(
|
||||||
|
request: RecentLogsRequest,
|
||||||
|
responseObserver: StreamObserver<LogEntries>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
val entries = backend.recentLogs(request.sinceUnixMillis, request.levelAtLeast)
|
val entries = backend.recentLogs(
|
||||||
|
request.sinceUnixMillis,
|
||||||
|
request.levelAtLeast,
|
||||||
|
)
|
||||||
val builder = LogEntries.newBuilder()
|
val builder = LogEntries.newBuilder()
|
||||||
for (entry in entries) {
|
for (entry in entries) {
|
||||||
builder.addEntries(
|
builder.addEntries(
|
||||||
@@ -123,7 +156,10 @@ class DriverService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun screenshot(request: Empty, responseObserver: StreamObserver<Image>) {
|
override fun screenshot(
|
||||||
|
request: Empty,
|
||||||
|
responseObserver: StreamObserver<Image>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
val (png, width, height) = backend.screenshot()
|
val (png, width, height) = backend.screenshot()
|
||||||
Image.newBuilder()
|
Image.newBuilder()
|
||||||
@@ -134,18 +170,28 @@ class DriverService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hierarchy(request: Empty, responseObserver: StreamObserver<HierarchyJSON>) {
|
override fun hierarchy(
|
||||||
|
request: Empty,
|
||||||
|
responseObserver: StreamObserver<HierarchyJSON>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
HierarchyJSON.newBuilder().setJson(backend.hierarchy()).build()
|
HierarchyJSON.newBuilder().setJson(backend.hierarchy()).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun snapshot(request: Empty, responseObserver: StreamObserver<SnapshotResponse>) {
|
override fun snapshot(
|
||||||
|
request: Empty,
|
||||||
|
responseObserver: StreamObserver<SnapshotResponse>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
val sample = synchronized(snapshotLock) { backend.snapshot() }
|
val sample = synchronized(snapshotLock) { backend.snapshot() }
|
||||||
val (png, width, height) = sample.screenshot
|
val (png, width, height) = sample.screenshot
|
||||||
SnapshotResponse.newBuilder()
|
SnapshotResponse.newBuilder()
|
||||||
.setHierarchy(HierarchyJSON.newBuilder().setJson(sample.hierarchyJson).build())
|
.setHierarchy(
|
||||||
|
HierarchyJSON.newBuilder()
|
||||||
|
.setJson(sample.hierarchyJson)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
.setScreenshot(
|
.setScreenshot(
|
||||||
Image.newBuilder()
|
Image.newBuilder()
|
||||||
.setPng(ByteString.copyFrom(png))
|
.setPng(ByteString.copyFrom(png))
|
||||||
@@ -157,14 +203,20 @@ class DriverService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun waitForIdle(request: Duration, responseObserver: StreamObserver<Empty>) {
|
override fun waitForIdle(
|
||||||
|
request: Duration,
|
||||||
|
responseObserver: StreamObserver<Empty>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.waitForIdle(request.millis)
|
backend.waitForIdle(request.millis)
|
||||||
Empty.getDefaultInstance()
|
Empty.getDefaultInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun health(request: Empty, responseObserver: StreamObserver<HealthStatus>) {
|
override fun health(
|
||||||
|
request: Empty,
|
||||||
|
responseObserver: StreamObserver<HealthStatus>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
HealthStatus.newBuilder()
|
HealthStatus.newBuilder()
|
||||||
.setReady(backend.healthy())
|
.setReady(backend.healthy())
|
||||||
@@ -174,9 +226,16 @@ class DriverService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun metrics(request: MetricsRequest, responseObserver: StreamObserver<MetricsResponse>) {
|
override fun metrics(
|
||||||
|
request: MetricsRequest,
|
||||||
|
responseObserver: StreamObserver<MetricsResponse>,
|
||||||
|
) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
val bundleId = if (request.bundleId.isNotEmpty()) request.bundleId else launchedBundleId.get().orEmpty()
|
val bundleId = if (request.bundleId.isNotEmpty()) {
|
||||||
|
request.bundleId
|
||||||
|
} else {
|
||||||
|
launchedBundleId.get().orEmpty()
|
||||||
|
}
|
||||||
val sample = backend.metrics(bundleId)
|
val sample = backend.metrics(bundleId)
|
||||||
MetricsResponse.newBuilder()
|
MetricsResponse.newBuilder()
|
||||||
.setCpuPercent(sample.cpuPercent)
|
.setCpuPercent(sample.cpuPercent)
|
||||||
@@ -191,7 +250,9 @@ class DriverService(
|
|||||||
// stale session, then closes the backend so the iOS XCTest runner process
|
// stale session, then closes the backend so the iOS XCTest runner process
|
||||||
// dies with us instead of being orphaned.
|
// dies with us instead of being orphaned.
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
runCatching { launchedBundleId.getAndSet(null)?.let { backend.terminate(it) } }
|
runCatching {
|
||||||
|
launchedBundleId.getAndSet(null)?.let { backend.terminate(it) }
|
||||||
|
}
|
||||||
runCatching { backend.close() }
|
runCatching { backend.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,8 +270,10 @@ class DriverService(
|
|||||||
// failures that do not extend Exception, and an uncaught one
|
// failures that do not extend Exception, and an uncaught one
|
||||||
// kills the RPC as a channel-level Unknown instead of a status
|
// kills the RPC as a channel-level Unknown instead of a status
|
||||||
// the runner can classify.
|
// the runner can classify.
|
||||||
observer.onError(io.grpc.Status.INTERNAL.withDescription(cause.toString())
|
observer.onError(
|
||||||
.withCause(cause).asRuntimeException())
|
io.grpc.Status.INTERNAL.withDescription(cause.toString())
|
||||||
|
.withCause(cause).asRuntimeException(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,17 @@ class SidecarServer(
|
|||||||
private val shutdownLatch = CountDownLatch(1)
|
private val shutdownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
fun start(): Int {
|
fun start(): Int {
|
||||||
val server = NettyServerBuilder.forAddress(InetSocketAddress("127.0.0.1", port))
|
val server = NettyServerBuilder
|
||||||
|
.forAddress(InetSocketAddress("127.0.0.1", port))
|
||||||
.addService(service)
|
.addService(service)
|
||||||
.build()
|
.build()
|
||||||
server.start()
|
server.start()
|
||||||
grpcServer = server
|
grpcServer = server
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
Runtime.getRuntime().addShutdownHook(
|
||||||
stop()
|
Thread {
|
||||||
})
|
stop()
|
||||||
|
},
|
||||||
|
)
|
||||||
return server.port
|
return server.port
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,23 +51,41 @@ class SidecarServer(
|
|||||||
// lost from run output.
|
// lost from run output.
|
||||||
private fun quietExpectedDriverNoise() {
|
private fun quietExpectedDriverNoise() {
|
||||||
org.apache.logging.log4j.core.config.Configurator.setLevel(
|
org.apache.logging.log4j.core.config.Configurator.setLevel(
|
||||||
"util.CommandLineUtils", org.apache.logging.log4j.Level.OFF)
|
"util.CommandLineUtils",
|
||||||
|
org.apache.logging.log4j.Level.OFF,
|
||||||
|
)
|
||||||
org.apache.logging.log4j.core.config.Configurator.setLevel(
|
org.apache.logging.log4j.core.config.Configurator.setLevel(
|
||||||
"xcuitest.XCTestDriverClient", org.apache.logging.log4j.Level.OFF)
|
"xcuitest.XCTestDriverClient",
|
||||||
|
org.apache.logging.log4j.Level.OFF,
|
||||||
|
)
|
||||||
org.apache.logging.log4j.core.config.Configurator.setLevel(
|
org.apache.logging.log4j.core.config.Configurator.setLevel(
|
||||||
"maestro.drivers.AndroidDriver", org.apache.logging.log4j.Level.OFF)
|
"maestro.drivers.AndroidDriver",
|
||||||
|
org.apache.logging.log4j.Level.OFF,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun main(arguments: Array<String>) {
|
fun main(arguments: Array<String>) {
|
||||||
quietExpectedDriverNoise()
|
quietExpectedDriverNoise()
|
||||||
val port = arguments.indexOf("--port").let { index ->
|
val port = arguments.indexOf("--port").let { index ->
|
||||||
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1].toInt() else 0
|
if (index >= 0 && index + 1 < arguments.size) {
|
||||||
|
arguments[index + 1].toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val platform = arguments.indexOf("--platform").let { index ->
|
val platform = arguments.indexOf("--platform").let { index ->
|
||||||
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else "android"
|
if (index >= 0 && index + 1 < arguments.size) {
|
||||||
|
arguments[index + 1]
|
||||||
|
} else {
|
||||||
|
"android"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val serial = arguments.indexOf("--serial").let { index ->
|
val serial = arguments.indexOf("--serial").let { index ->
|
||||||
if (index >= 0 && index + 1 < arguments.size) arguments[index + 1] else null
|
if (index >= 0 && index + 1 < arguments.size) {
|
||||||
|
arguments[index + 1]
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val backend: DriverBackend = when (platform) {
|
val backend: DriverBackend = when (platform) {
|
||||||
@@ -74,7 +95,9 @@ fun main(arguments: Array<String>) {
|
|||||||
val service = DriverService(platform = platform, backend = backend)
|
val service = DriverService(platform = platform, backend = backend)
|
||||||
val server = SidecarServer(port, service)
|
val server = SidecarServer(port, service)
|
||||||
val boundPort = server.start()
|
val boundPort = server.start()
|
||||||
println("sanderling-sidecar listening on 127.0.0.1:$boundPort platform=$platform")
|
println(
|
||||||
|
"sanderling-sidecar listening on 127.0.0.1:$boundPort platform=$platform",
|
||||||
|
)
|
||||||
System.out.flush()
|
System.out.flush()
|
||||||
server.awaitTermination()
|
server.awaitTermination()
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,10 @@ class DadbTargetTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test fun hostPortSerialConnectsDirectly() {
|
@Test fun hostPortSerialConnectsDirectly() {
|
||||||
assertEquals(DadbTarget.Tcp("192.168.1.243", 5555), dadbTargetFor("192.168.1.243:5555"))
|
assertEquals(
|
||||||
|
DadbTarget.Tcp("192.168.1.243", 5555),
|
||||||
|
dadbTargetFor("192.168.1.243:5555"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun usbSerialRoutesThroughAdbServer() {
|
@Test fun usbSerialRoutesThroughAdbServer() {
|
||||||
@@ -20,6 +23,9 @@ class DadbTargetTest {
|
|||||||
// A colon with a non-numeric port is a USB serial that merely contains a
|
// A colon with a non-numeric port is a USB serial that merely contains a
|
||||||
// colon, not a host:port, so it must route through the adb server.
|
// colon, not a host:port, so it must route through the adb server.
|
||||||
@Test fun colonWithNonNumericPortIsAServerSerial() {
|
@Test fun colonWithNonNumericPortIsAServerSerial() {
|
||||||
assertEquals(DadbTarget.Server("emulator:5554x"), dadbTargetFor("emulator:5554x"))
|
assertEquals(
|
||||||
|
DadbTarget.Server("emulator:5554x"),
|
||||||
|
dadbTargetFor("emulator:5554x"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,8 @@ class DeviceOutputParserTest {
|
|||||||
assertEquals("FATAL EXCEPTION: main", lines[0].message)
|
assertEquals("FATAL EXCEPTION: main", lines[0].message)
|
||||||
|
|
||||||
val year = java.util.Calendar.getInstance().get(java.util.Calendar.YEAR)
|
val year = java.util.Calendar.getInstance().get(java.util.Calendar.YEAR)
|
||||||
val cal = java.util.Calendar.getInstance().apply { timeInMillis = lines[0].unixMillis }
|
val cal = java.util.Calendar.getInstance()
|
||||||
|
.apply { timeInMillis = lines[0].unixMillis }
|
||||||
assertEquals(year, cal.get(java.util.Calendar.YEAR))
|
assertEquals(year, cal.get(java.util.Calendar.YEAR))
|
||||||
assertEquals(56, cal.get(java.util.Calendar.SECOND))
|
assertEquals(56, cal.get(java.util.Calendar.SECOND))
|
||||||
assertEquals(789, cal.get(java.util.Calendar.MILLISECOND))
|
assertEquals(789, cal.get(java.util.Calendar.MILLISECOND))
|
||||||
@@ -51,7 +52,9 @@ class DeviceOutputParserTest {
|
|||||||
|
|
||||||
@Test fun parseCpuTicksReturnsNullOnTruncatedOrNonNumericStat() {
|
@Test fun parseCpuTicksReturnsNullOnTruncatedOrNonNumericStat() {
|
||||||
assertNull(parseCpuTicks("1234 (app) S 1 2 3"))
|
assertNull(parseCpuTicks("1234 (app) S 1 2 3"))
|
||||||
assertNull(parseCpuTicks("1234 (app) S " + (1..12).joinToString(" ") { "x" }))
|
assertNull(
|
||||||
|
parseCpuTicks("1234 (app) S " + (1..12).joinToString(" ") { "x" }),
|
||||||
|
)
|
||||||
assertNull(parseCpuTicks(""))
|
assertNull(parseCpuTicks(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +82,14 @@ class DeviceOutputParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test fun parseBoundsAcceptsWellFormedAndRejectsMalformed() {
|
@Test fun parseBoundsAcceptsWellFormedAndRejectsMalformed() {
|
||||||
assertEquals(listOf(0, 0, 1080, 2340), parseBounds("[0,0,1080,2340]")?.toList())
|
assertEquals(
|
||||||
assertEquals(listOf(-5, -10, 20, 30), parseBounds("[-5,-10,20,30]")?.toList())
|
listOf(0, 0, 1080, 2340),
|
||||||
|
parseBounds("[0,0,1080,2340]")?.toList(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(-5, -10, 20, 30),
|
||||||
|
parseBounds("[-5,-10,20,30]")?.toList(),
|
||||||
|
)
|
||||||
assertNull(parseBounds("[0,0,1080]"))
|
assertNull(parseBounds("[0,0,1080]"))
|
||||||
assertNull(parseBounds("0,0,1,1"))
|
assertNull(parseBounds("0,0,1,1"))
|
||||||
assertNull(parseBounds("[0, 0, 1, 1]"))
|
assertNull(parseBounds("[0, 0, 1, 1]"))
|
||||||
@@ -92,10 +101,14 @@ class DeviceOutputParserTest {
|
|||||||
"resource-id" to "com.example:id/loginButton",
|
"resource-id" to "com.example:id/loginButton",
|
||||||
"bounds" to "[10,20,110,80]",
|
"bounds" to "[10,20,110,80]",
|
||||||
)
|
)
|
||||||
assertEquals(listOf(10, 20, 110, 80), findBoundsBySelector(tree, "id:loginButton")?.toList())
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(10, 20, 110, 80),
|
listOf(10, 20, 110, 80),
|
||||||
findBoundsBySelector(tree, "id:com.example:id/loginButton")?.toList(),
|
findBoundsBySelector(tree, "id:loginButton")?.toList(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(10, 20, 110, 80),
|
||||||
|
findBoundsBySelector(tree, "id:com.example:id/loginButton")
|
||||||
|
?.toList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,21 +117,36 @@ class DeviceOutputParserTest {
|
|||||||
"resource-id" to "root",
|
"resource-id" to "root",
|
||||||
children = listOf(
|
children = listOf(
|
||||||
node("text" to "Sign in", "bounds" to "[1,2,3,4]"),
|
node("text" to "Sign in", "bounds" to "[1,2,3,4]"),
|
||||||
node("content-desc" to "AccountCardRow-7", "bounds" to "[5,6,7,8]"),
|
node(
|
||||||
|
"content-desc" to "AccountCardRow-7",
|
||||||
|
"bounds" to "[5,6,7,8]",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertEquals(listOf(1, 2, 3, 4), findBoundsBySelector(tree, "text:Sign in")?.toList())
|
assertEquals(
|
||||||
assertEquals(listOf(5, 6, 7, 8), findBoundsBySelector(tree, "descPrefix:AccountCard")?.toList())
|
listOf(1, 2, 3, 4),
|
||||||
|
findBoundsBySelector(tree, "text:Sign in")?.toList(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(5, 6, 7, 8),
|
||||||
|
findBoundsBySelector(tree, "descPrefix:AccountCard")?.toList(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun findBoundsBySelectorReturnsNullForBadSelectorOrNoMatch() {
|
@Test fun findBoundsBySelectorReturnsNullForBadSelectorOrNoMatch() {
|
||||||
val tree = node("resource-id" to "com.example:id/x", "bounds" to "[0,0,1,1]")
|
val tree = node(
|
||||||
|
"resource-id" to "com.example:id/x",
|
||||||
|
"bounds" to "[0,0,1,1]",
|
||||||
|
)
|
||||||
assertNull(findBoundsBySelector(tree, "id"))
|
assertNull(findBoundsBySelector(tree, "id"))
|
||||||
assertNull(findBoundsBySelector(tree, "id:missing"))
|
assertNull(findBoundsBySelector(tree, "id:missing"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun findBoundsBySelectorReturnsNullWhenMatchHasMalformedBounds() {
|
@Test fun findBoundsBySelectorReturnsNullWhenMatchHasMalformedBounds() {
|
||||||
val tree = node("resource-id" to "com.example:id/x", "bounds" to "not-bounds")
|
val tree = node(
|
||||||
|
"resource-id" to "com.example:id/x",
|
||||||
|
"bounds" to "not-bounds",
|
||||||
|
)
|
||||||
assertNull(findBoundsBySelector(tree, "id:x"))
|
assertNull(findBoundsBySelector(tree, "id:x"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,17 +160,26 @@ class DeviceOutputParserTest {
|
|||||||
private fun ihdr(width: Int, height: Int): ByteArray {
|
private fun ihdr(width: Int, height: Int): ByteArray {
|
||||||
val b = ByteArray(33)
|
val b = ByteArray(33)
|
||||||
for (i in 0 until 8) b[8 + i] = 0
|
for (i in 0 until 8) b[8 + i] = 0
|
||||||
b[12] = 'I'.code.toByte(); b[13] = 'H'.code.toByte()
|
b[12] = 'I'.code.toByte()
|
||||||
b[14] = 'D'.code.toByte(); b[15] = 'R'.code.toByte()
|
b[13] = 'H'.code.toByte()
|
||||||
b[16] = (width ushr 24).toByte(); b[17] = (width ushr 16).toByte()
|
b[14] = 'D'.code.toByte()
|
||||||
b[18] = (width ushr 8).toByte(); b[19] = width.toByte()
|
b[15] = 'R'.code.toByte()
|
||||||
b[20] = (height ushr 24).toByte(); b[21] = (height ushr 16).toByte()
|
b[16] = (width ushr 24).toByte()
|
||||||
b[22] = (height ushr 8).toByte(); b[23] = height.toByte()
|
b[17] = (width ushr 16).toByte()
|
||||||
|
b[18] = (width ushr 8).toByte()
|
||||||
|
b[19] = width.toByte()
|
||||||
|
b[20] = (height ushr 24).toByte()
|
||||||
|
b[21] = (height ushr 16).toByte()
|
||||||
|
b[22] = (height ushr 8).toByte()
|
||||||
|
b[23] = height.toByte()
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun node(
|
private fun node(
|
||||||
vararg attrs: Pair<String, String>,
|
vararg attrs: Pair<String, String>,
|
||||||
children: List<maestro.TreeNode> = emptyList(),
|
children: List<maestro.TreeNode> = emptyList(),
|
||||||
): maestro.TreeNode = maestro.TreeNode(attributes = attrs.toMap().toMutableMap(), children = children)
|
): maestro.TreeNode = maestro.TreeNode(
|
||||||
|
attributes = attrs.toMap().toMutableMap(),
|
||||||
|
children = children,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -20,20 +20,34 @@ import org.junit.Test
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
private data class Quintuple<A, B, C, D, E>(val a: A, val b: B, val c: C, val d: D, val e: E)
|
private data class Quintuple<A, B, C, D, E>(
|
||||||
|
val a: A,
|
||||||
|
val b: B,
|
||||||
|
val c: C,
|
||||||
|
val d: D,
|
||||||
|
val e: E,
|
||||||
|
)
|
||||||
|
|
||||||
class DriverServiceTest {
|
class DriverServiceTest {
|
||||||
|
|
||||||
@get:Rule val grpcCleanup: GrpcCleanupRule = GrpcCleanupRule()
|
@get:Rule val grpcCleanup: GrpcCleanupRule = GrpcCleanupRule()
|
||||||
|
|
||||||
private fun newClient(backend: DriverBackend): DriverGrpc.DriverBlockingStub {
|
private fun newClient(
|
||||||
|
backend: DriverBackend,
|
||||||
|
): DriverGrpc.DriverBlockingStub {
|
||||||
val serverName = InProcessServerBuilder.generateName()
|
val serverName = InProcessServerBuilder.generateName()
|
||||||
val service = DriverService(platform = "android", backend = backend)
|
val service = DriverService(platform = "android", backend = backend)
|
||||||
grpcCleanup.register(
|
grpcCleanup.register(
|
||||||
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start()
|
InProcessServerBuilder.forName(serverName)
|
||||||
|
.directExecutor()
|
||||||
|
.addService(service)
|
||||||
|
.build()
|
||||||
|
.start(),
|
||||||
)
|
)
|
||||||
val channel: ManagedChannel = grpcCleanup.register(
|
val channel: ManagedChannel = grpcCleanup.register(
|
||||||
InProcessChannelBuilder.forName(serverName).directExecutor().build()
|
InProcessChannelBuilder.forName(serverName)
|
||||||
|
.directExecutor()
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
return DriverGrpc.newBlockingStub(channel)
|
return DriverGrpc.newBlockingStub(channel)
|
||||||
}
|
}
|
||||||
@@ -59,20 +73,32 @@ class DriverServiceTest {
|
|||||||
var terminated: String? = null
|
var terminated: String? = null
|
||||||
var closed = false
|
var closed = false
|
||||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||||
override fun terminate(bundleId: String) { terminated = bundleId }
|
override fun terminate(bundleId: String) {
|
||||||
override fun close() { closed = true }
|
terminated = bundleId
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val serverName = InProcessServerBuilder.generateName()
|
val serverName = InProcessServerBuilder.generateName()
|
||||||
val service = DriverService(platform = "android", backend = backend)
|
val service = DriverService(platform = "android", backend = backend)
|
||||||
grpcCleanup.register(
|
grpcCleanup.register(
|
||||||
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start()
|
InProcessServerBuilder.forName(serverName)
|
||||||
|
.directExecutor()
|
||||||
|
.addService(service)
|
||||||
|
.build()
|
||||||
|
.start(),
|
||||||
)
|
)
|
||||||
val channel: ManagedChannel = grpcCleanup.register(
|
val channel: ManagedChannel = grpcCleanup.register(
|
||||||
InProcessChannelBuilder.forName(serverName).directExecutor().build()
|
InProcessChannelBuilder.forName(serverName)
|
||||||
|
.directExecutor()
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
val client = DriverGrpc.newBlockingStub(channel)
|
val client = DriverGrpc.newBlockingStub(channel)
|
||||||
|
|
||||||
client.launch(LaunchRequest.newBuilder().setBundleId("com.example").build())
|
client.launch(
|
||||||
|
LaunchRequest.newBuilder().setBundleId("com.example").build(),
|
||||||
|
)
|
||||||
service.shutdown()
|
service.shutdown()
|
||||||
|
|
||||||
assertEquals("com.example", terminated)
|
assertEquals("com.example", terminated)
|
||||||
@@ -83,8 +109,12 @@ class DriverServiceTest {
|
|||||||
var terminated: String? = null
|
var terminated: String? = null
|
||||||
var closed = false
|
var closed = false
|
||||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||||
override fun terminate(bundleId: String) { terminated = bundleId }
|
override fun terminate(bundleId: String) {
|
||||||
override fun close() { closed = true }
|
terminated = bundleId
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val service = DriverService(platform = "android", backend = backend)
|
val service = DriverService(platform = "android", backend = backend)
|
||||||
|
|
||||||
@@ -128,17 +158,17 @@ class DriverServiceTest {
|
|||||||
// the runner can tell transient failures from fatal ones.
|
// the runner can tell transient failures from fatal ones.
|
||||||
@Test fun backendStatusCodePassesThrough() {
|
@Test fun backendStatusCodePassesThrough() {
|
||||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||||
override fun inputText(text: String) {
|
override fun inputText(text: String): Unit =
|
||||||
throw io.grpc.Status.UNAVAILABLE
|
throw io.grpc.Status.UNAVAILABLE
|
||||||
.withDescription("connection dropped mid-action")
|
.withDescription("connection dropped mid-action")
|
||||||
.asRuntimeException()
|
.asRuntimeException()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
val thrown = kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
|
val thrown =
|
||||||
client.inputText(Text.newBuilder().setValue("hello").build())
|
kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
|
||||||
}
|
client.inputText(Text.newBuilder().setValue("hello").build())
|
||||||
|
}
|
||||||
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
|
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,17 +177,19 @@ class DriverServiceTest {
|
|||||||
// channel-level Unknown the runner cannot classify.
|
// channel-level Unknown the runner cannot classify.
|
||||||
@Test fun nonExceptionThrowableMapsToInternal() {
|
@Test fun nonExceptionThrowableMapsToInternal() {
|
||||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||||
override fun inputText(text: String) {
|
override fun inputText(text: String): Unit =
|
||||||
throw Throwable("only one gesture can be performed at a time")
|
throw Throwable("only one gesture can be performed at a time")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
val thrown = kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
|
val thrown =
|
||||||
client.inputText(Text.newBuilder().setValue("hello").build())
|
kotlin.test.assertFailsWith<io.grpc.StatusRuntimeException> {
|
||||||
}
|
client.inputText(Text.newBuilder().setValue("hello").build())
|
||||||
|
}
|
||||||
assertEquals(io.grpc.Status.Code.INTERNAL, thrown.status.code)
|
assertEquals(io.grpc.Status.Code.INTERNAL, thrown.status.code)
|
||||||
assertTrue(thrown.status.description.orEmpty().contains("only one gesture"))
|
assertTrue(
|
||||||
|
thrown.status.description.orEmpty().contains("only one gesture"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun reapOrphanIosRunnersKillsStrayXcodebuildAndRunnerApp() {
|
@Test fun reapOrphanIosRunnersKillsStrayXcodebuildAndRunnerApp() {
|
||||||
@@ -171,7 +203,16 @@ class DriverServiceTest {
|
|||||||
assertEquals("pkill", commands[0][0])
|
assertEquals("pkill", commands[0][0])
|
||||||
assertTrue(commands[0][2].contains("test-without-building"))
|
assertTrue(commands[0][2].contains("test-without-building"))
|
||||||
assertTrue(commands[0][2].contains("UDID-1234"))
|
assertTrue(commands[0][2].contains("UDID-1234"))
|
||||||
assertEquals(listOf("xcrun", "simctl", "terminate", "UDID-1234", IOS_XCTEST_RUNNER_BUNDLE_ID), commands[1])
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
"xcrun",
|
||||||
|
"simctl",
|
||||||
|
"terminate",
|
||||||
|
"UDID-1234",
|
||||||
|
IOS_XCTEST_RUNNER_BUNDLE_ID,
|
||||||
|
),
|
||||||
|
commands[1],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun reapOrphanIosRunnersReportsNothingFound() {
|
@Test fun reapOrphanIosRunnersReportsNothingFound() {
|
||||||
@@ -192,7 +233,9 @@ class DriverServiceTest {
|
|||||||
// still executing fails instead of queuing.
|
// still executing fails instead of queuing.
|
||||||
val tapAction = {
|
val tapAction = {
|
||||||
if (!inFlight.compareAndSet(false, true)) {
|
if (!inFlight.compareAndSet(false, true)) {
|
||||||
throw IllegalStateException("only one gesture can be performed at a time")
|
throw IllegalStateException(
|
||||||
|
"only one gesture can be performed at a time",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
invocations.incrementAndGet()
|
invocations.incrementAndGet()
|
||||||
Thread.sleep(150)
|
Thread.sleep(150)
|
||||||
@@ -211,7 +254,9 @@ class DriverServiceTest {
|
|||||||
val tapAction = {
|
val tapAction = {
|
||||||
if (failedFirst.compareAndSet(false, true)) {
|
if (failedFirst.compareAndSet(false, true)) {
|
||||||
Thread.sleep(60)
|
Thread.sleep(60)
|
||||||
throw IllegalStateException("only one gesture can be performed at a time")
|
throw IllegalStateException(
|
||||||
|
"only one gesture can be performed at a time",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
landed.incrementAndGet()
|
landed.incrementAndGet()
|
||||||
Unit
|
Unit
|
||||||
@@ -226,21 +271,38 @@ class DriverServiceTest {
|
|||||||
// directly.
|
// directly.
|
||||||
val taps = mutableListOf<Pair<Int, Int>>()
|
val taps = mutableListOf<Pair<Int, Int>>()
|
||||||
val backend = object : DriverBackend {
|
val backend = object : DriverBackend {
|
||||||
override fun launch(bundleId: String, clearState: Boolean, env: Map<String, String>) {}
|
override fun launch(
|
||||||
|
bundleId: String,
|
||||||
|
clearState: Boolean,
|
||||||
|
env: Map<String, String>,
|
||||||
|
) {}
|
||||||
override fun terminate(bundleId: String) {}
|
override fun terminate(bundleId: String) {}
|
||||||
override fun tap(x: Int, y: Int) { taps.add(x to y) }
|
override fun tap(x: Int, y: Int) {
|
||||||
|
taps.add(x to y)
|
||||||
|
}
|
||||||
override fun tapSelector(selector: String) {}
|
override fun tapSelector(selector: String) {}
|
||||||
override fun inputText(text: String) {}
|
override fun inputText(text: String) {}
|
||||||
override fun eraseText(characterCount: Int) {}
|
override fun eraseText(characterCount: Int) {}
|
||||||
override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) {}
|
override fun swipe(
|
||||||
|
fromX: Int,
|
||||||
|
fromY: Int,
|
||||||
|
toX: Int,
|
||||||
|
toY: Int,
|
||||||
|
durationMillis: Long,
|
||||||
|
) {}
|
||||||
override fun pressKey(key: String) {}
|
override fun pressKey(key: String) {}
|
||||||
override fun longPress(x: Int, y: Int) {}
|
override fun longPress(x: Int, y: Int) {}
|
||||||
override fun screenshot(): Triple<ByteArray, Int, Int> = Triple(byteArrayOf(), 0, 0)
|
override fun screenshot(): Triple<ByteArray, Int, Int> =
|
||||||
|
Triple(byteArrayOf(), 0, 0)
|
||||||
override fun hierarchy(): String = "{}"
|
override fun hierarchy(): String = "{}"
|
||||||
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> = emptyList()
|
override fun recentLogs(
|
||||||
|
sinceUnixMillis: Long,
|
||||||
|
minLevel: String,
|
||||||
|
): List<LogLine> = emptyList()
|
||||||
override fun waitForIdle(durationMillis: Long) {}
|
override fun waitForIdle(durationMillis: Long) {}
|
||||||
override fun healthy(): Boolean = true
|
override fun healthy(): Boolean = true
|
||||||
override fun metrics(bundleId: String): MetricsSample = MetricsSample(0.0, 0L, 0L)
|
override fun metrics(bundleId: String): MetricsSample =
|
||||||
|
MetricsSample(0.0, 0L, 0L)
|
||||||
}
|
}
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
@@ -252,13 +314,16 @@ class DriverServiceTest {
|
|||||||
val backend = StubDriverBackend("android")
|
val backend = StubDriverBackend("android")
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
client.eraseText(EraseTextRequest.newBuilder().setCharacterCount(11).build())
|
client.eraseText(
|
||||||
|
EraseTextRequest.newBuilder().setCharacterCount(11).build(),
|
||||||
|
)
|
||||||
assertEquals(11, backend.lastEraseCharacterCount)
|
assertEquals(11, backend.lastEraseCharacterCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun screenshotReturnsBackendBytes() {
|
@Test fun screenshotReturnsBackendBytes() {
|
||||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||||
override fun screenshot(): Triple<ByteArray, Int, Int> = Triple(byteArrayOf(1, 2, 3), 1080, 2340)
|
override fun screenshot(): Triple<ByteArray, Int, Int> =
|
||||||
|
Triple(byteArrayOf(1, 2, 3), 1080, 2340)
|
||||||
}
|
}
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
@@ -294,7 +359,13 @@ class DriverServiceTest {
|
|||||||
@Test fun swipeForwardsEndpointsAndDuration() {
|
@Test fun swipeForwardsEndpointsAndDuration() {
|
||||||
var observed: Quintuple<Int, Int, Int, Int, Long>? = null
|
var observed: Quintuple<Int, Int, Int, Int, Long>? = null
|
||||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||||
override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) {
|
override fun swipe(
|
||||||
|
fromX: Int,
|
||||||
|
fromY: Int,
|
||||||
|
toX: Int,
|
||||||
|
toY: Int,
|
||||||
|
durationMillis: Long,
|
||||||
|
) {
|
||||||
observed = Quintuple(fromX, fromY, toX, toY, durationMillis)
|
observed = Quintuple(fromX, fromY, toX, toY, durationMillis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,14 +396,18 @@ class DriverServiceTest {
|
|||||||
|
|
||||||
@Test fun recentLogsReturnsBackendEntries() {
|
@Test fun recentLogsReturnsBackendEntries() {
|
||||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||||
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> {
|
override fun recentLogs(
|
||||||
return listOf(LogLine(1, "E", "AndroidRuntime", "boom"))
|
sinceUnixMillis: Long,
|
||||||
}
|
minLevel: String,
|
||||||
|
): List<LogLine> = listOf(LogLine(1, "E", "AndroidRuntime", "boom"))
|
||||||
}
|
}
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
val response = client.recentLogs(
|
val response = client.recentLogs(
|
||||||
RecentLogsRequest.newBuilder().setSinceUnixMillis(0).setLevelAtLeast("E").build(),
|
RecentLogsRequest.newBuilder()
|
||||||
|
.setSinceUnixMillis(0)
|
||||||
|
.setLevelAtLeast("E")
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
assertEquals(1, response.entriesCount)
|
assertEquals(1, response.entriesCount)
|
||||||
assertEquals("AndroidRuntime", response.getEntries(0).tag)
|
assertEquals("AndroidRuntime", response.getEntries(0).tag)
|
||||||
@@ -351,7 +426,9 @@ class DriverServiceTest {
|
|||||||
}
|
}
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
client.launch(LaunchRequest.newBuilder().setBundleId("com.launched").build())
|
client.launch(
|
||||||
|
LaunchRequest.newBuilder().setBundleId("com.launched").build(),
|
||||||
|
)
|
||||||
client.metrics(MetricsRequest.getDefaultInstance())
|
client.metrics(MetricsRequest.getDefaultInstance())
|
||||||
|
|
||||||
assertEquals("com.launched", sampled)
|
assertEquals("com.launched", sampled)
|
||||||
@@ -367,8 +444,12 @@ class DriverServiceTest {
|
|||||||
}
|
}
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
client.launch(LaunchRequest.newBuilder().setBundleId("com.launched").build())
|
client.launch(
|
||||||
client.metrics(MetricsRequest.newBuilder().setBundleId("com.other").build())
|
LaunchRequest.newBuilder().setBundleId("com.launched").build(),
|
||||||
|
)
|
||||||
|
client.metrics(
|
||||||
|
MetricsRequest.newBuilder().setBundleId("com.other").build(),
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals("com.other", sampled)
|
assertEquals("com.other", sampled)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ class InputTextTest {
|
|||||||
val fallback = listOf(
|
val fallback = listOf(
|
||||||
"Emergency Fund", "🙂🔥💸", " ", "\t\n", "'; DROP TABLE--",
|
"Emergency Fund", "🙂🔥💸", " ", "\t\n", "'; DROP TABLE--",
|
||||||
"<script>alert(1)</script>", "../../etc/passwd", "%s%n", "",
|
"<script>alert(1)</script>", "../../etc/passwd", "%s%n", "",
|
||||||
"-1", "-rf", // a leading dash could be read as an option by `input text`
|
// a leading dash could be read as an option by `input text`
|
||||||
|
"-1", "-rf",
|
||||||
)
|
)
|
||||||
for (text in fallback) {
|
for (text in fallback) {
|
||||||
assertTrue(
|
assertTrue(
|
||||||
@@ -190,12 +191,12 @@ class InputTextTest {
|
|||||||
|
|
||||||
@Test fun parseResumedPackageReadsEachResumedActivityWording() {
|
@Test fun parseResumedPackageReadsEachResumedActivityWording() {
|
||||||
val cases = mapOf(
|
val cases = mapOf(
|
||||||
" topResumedActivity=ActivityRecord{8b u0 app.folio/.MainActivity t42}" to
|
" topResumedActivity=ActivityRecord{8b u0 " +
|
||||||
"app.folio",
|
"app.folio/.MainActivity t42}" to "app.folio",
|
||||||
" mResumedActivity: ActivityRecord{1c u0 com.example.app/.Home t9}" to
|
" mResumedActivity: ActivityRecord{1c u0 " +
|
||||||
"com.example.app",
|
"com.example.app/.Home t9}" to "com.example.app",
|
||||||
" ResumedActivity: ActivityRecord{2d u0 app.folio/com.folio.Detail t9}" to
|
" ResumedActivity: ActivityRecord{2d u0 " +
|
||||||
"app.folio",
|
"app.folio/com.folio.Detail t9}" to "app.folio",
|
||||||
)
|
)
|
||||||
for ((line, want) in cases) {
|
for ((line, want) in cases) {
|
||||||
assertEquals(want, parseResumedPackage(line), line)
|
assertEquals(want, parseResumedPackage(line), line)
|
||||||
|
|||||||
@@ -12,28 +12,40 @@ class ResolveActivityTest {
|
|||||||
com.example.app/.MainActivity
|
com.example.app/.MainActivity
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output)
|
val activity = StubDriverBackend.parseResolvedActivity(
|
||||||
|
"com.example.app",
|
||||||
|
output,
|
||||||
|
)
|
||||||
assertEquals(".MainActivity", activity)
|
assertEquals(".MainActivity", activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun extractsFullyQualifiedActivity() {
|
@Test fun extractsFullyQualifiedActivity() {
|
||||||
val output = "com.example.app/com.example.app.ui.LaunchActivity"
|
val output = "com.example.app/com.example.app.ui.LaunchActivity"
|
||||||
|
|
||||||
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output)
|
val activity = StubDriverBackend.parseResolvedActivity(
|
||||||
|
"com.example.app",
|
||||||
|
output,
|
||||||
|
)
|
||||||
assertEquals("com.example.app.ui.LaunchActivity", activity)
|
assertEquals("com.example.app.ui.LaunchActivity", activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun returnsNullWhenPackageNotFound() {
|
@Test fun returnsNullWhenPackageNotFound() {
|
||||||
val output = "No activity found"
|
val output = "No activity found"
|
||||||
|
|
||||||
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output)
|
val activity = StubDriverBackend.parseResolvedActivity(
|
||||||
|
"com.example.app",
|
||||||
|
output,
|
||||||
|
)
|
||||||
assertNull(activity)
|
assertNull(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun doesNotMatchDifferentPackagePrefix() {
|
@Test fun doesNotMatchDifferentPackagePrefix() {
|
||||||
val output = "other.pkg/.MainActivity"
|
val output = "other.pkg/.MainActivity"
|
||||||
|
|
||||||
val activity = StubDriverBackend.parseResolvedActivity("com.example.app", output)
|
val activity = StubDriverBackend.parseResolvedActivity(
|
||||||
|
"com.example.app",
|
||||||
|
output,
|
||||||
|
)
|
||||||
assertNull(activity)
|
assertNull(activity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,10 +13,13 @@ class RouteTransitionTest {
|
|||||||
private fun screen(id: String, child: String = "") =
|
private fun screen(id: String, child: String = "") =
|
||||||
"""{"attributes":{"resource-id":"$id"},"children":[$child]}"""
|
"""{"attributes":{"resource-id":"$id"},"children":[$child]}"""
|
||||||
|
|
||||||
private fun tree(vararg children: String) =
|
private fun tree(vararg children: String): String {
|
||||||
"""{"attributes":{"resource-id":"root"},"children":[${children.joinToString(",")}]}"""
|
val joined = children.joinToString(",")
|
||||||
|
return """{"attributes":{"resource-id":"root"},"children":[$joined]}"""
|
||||||
|
}
|
||||||
|
|
||||||
private val crossFade = tree(screen("LedgerScreen"), screen("AddTransactionScreen"))
|
private val crossFade =
|
||||||
|
tree(screen("LedgerScreen"), screen("AddTransactionScreen"))
|
||||||
private val landed = tree(screen("AddTransactionScreen"))
|
private val landed = tree(screen("AddTransactionScreen"))
|
||||||
|
|
||||||
@Test fun waitsForTheCrossFadeToLandAndReturnsTheLandedTree() {
|
@Test fun waitsForTheCrossFadeToLandAndReturnsTheLandedTree() {
|
||||||
@@ -29,8 +32,15 @@ class RouteTransitionTest {
|
|||||||
reads++
|
reads++
|
||||||
if (reads <= 3) crossFade else landed
|
if (reads <= 3) crossFade else landed
|
||||||
}
|
}
|
||||||
assertTrue(reads > 3, "must keep reading until the fade lands, reads=$reads")
|
assertTrue(
|
||||||
assertEquals(1, countRouteScreens(settled), "must return a tree with one route")
|
reads > 3,
|
||||||
|
"must keep reading until the fade lands, reads=$reads",
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
1,
|
||||||
|
countRouteScreens(settled),
|
||||||
|
"must return a tree with one route",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun settledFrameCostsExactlyOneRead() {
|
@Test fun settledFrameCostsExactlyOneRead() {
|
||||||
@@ -54,13 +64,21 @@ class RouteTransitionTest {
|
|||||||
// would burn the whole poll budget and still hand over a frame the
|
// would burn the whole poll budget and still hand over a frame the
|
||||||
// runner refuses to act on.
|
// runner refuses to act on.
|
||||||
val nested = tree(screen("HomeScreen", screen("HomeScreen")))
|
val nested = tree(screen("HomeScreen", screen("HomeScreen")))
|
||||||
assertEquals(1, countRouteScreens(nested), "the same id twice is one route")
|
assertEquals(
|
||||||
|
1,
|
||||||
|
countRouteScreens(nested),
|
||||||
|
"the same id twice is one route",
|
||||||
|
)
|
||||||
var reads = 0
|
var reads = 0
|
||||||
awaitSettledTree {
|
awaitSettledTree {
|
||||||
reads++
|
reads++
|
||||||
nested
|
nested
|
||||||
}
|
}
|
||||||
assertEquals(1, reads, "a repeated route id must not be treated as a transition")
|
assertEquals(
|
||||||
|
1,
|
||||||
|
reads,
|
||||||
|
"a repeated route id must not be treated as a transition",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun aLayoutThatKeepsTwoRoutesIsBoundedByTheCap() {
|
@Test fun aLayoutThatKeepsTwoRoutesIsBoundedByTheCap() {
|
||||||
@@ -78,7 +96,11 @@ class RouteTransitionTest {
|
|||||||
elapsed < TRANSITION_POLL_CAP_MILLIS + 1000L,
|
elapsed < TRANSITION_POLL_CAP_MILLIS + 1000L,
|
||||||
"must stop at the cap, elapsed=${elapsed}ms",
|
"must stop at the cap, elapsed=${elapsed}ms",
|
||||||
)
|
)
|
||||||
assertEquals(crossFade, settled, "the caller still gets a tree to record")
|
assertEquals(
|
||||||
|
crossFade,
|
||||||
|
settled,
|
||||||
|
"the caller still gets a tree to record",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun capCoversTheNavHostFadePlusTheStreak() {
|
@Test fun capCoversTheNavHostFadePlusTheStreak() {
|
||||||
@@ -89,20 +111,29 @@ class RouteTransitionTest {
|
|||||||
val fadeMillis = 700L
|
val fadeMillis = 700L
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val settled = awaitSettledTree {
|
val settled = awaitSettledTree {
|
||||||
if (System.currentTimeMillis() - start < fadeMillis) crossFade else landed
|
if (System.currentTimeMillis() - start < fadeMillis) {
|
||||||
|
crossFade
|
||||||
|
} else {
|
||||||
|
landed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val elapsed = System.currentTimeMillis() - start
|
val elapsed = System.currentTimeMillis() - start
|
||||||
|
|
||||||
assertEquals(landed, settled, "must hand back the landed tree, not the fade")
|
assertEquals(
|
||||||
|
landed,
|
||||||
|
settled,
|
||||||
|
"must hand back the landed tree, not the fade",
|
||||||
|
)
|
||||||
assertTrue(
|
assertTrue(
|
||||||
elapsed >= fadeMillis,
|
elapsed >= fadeMillis,
|
||||||
"cannot have settled before the fade ended, elapsed=${elapsed}ms",
|
"cannot have settled before the fade ended, elapsed=${elapsed}ms",
|
||||||
)
|
)
|
||||||
assertTrue(
|
assertTrue(
|
||||||
elapsed < TRANSITION_POLL_CAP_MILLIS,
|
elapsed < TRANSITION_POLL_CAP_MILLIS,
|
||||||
"the ${TRANSITION_POLL_CAP_MILLIS}ms cap has to leave room for a ${fadeMillis}ms " +
|
"the ${TRANSITION_POLL_CAP_MILLIS}ms cap has to leave room for " +
|
||||||
"fade and the ${TRANSITION_STABLE_STREAK_MILLIS}ms streak after it, but the " +
|
"a ${fadeMillis}ms fade and the " +
|
||||||
"wait ran to the cap instead, elapsed=${elapsed}ms",
|
"${TRANSITION_STABLE_STREAK_MILLIS}ms streak after it, but " +
|
||||||
|
"the wait ran to the cap instead, elapsed=${elapsed}ms",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,10 @@ import kotlin.test.assertTrue
|
|||||||
class SidecarServerTest {
|
class SidecarServerTest {
|
||||||
@Test
|
@Test
|
||||||
fun startBindsEphemeralPortAndStopReleasesIt() {
|
fun startBindsEphemeralPortAndStopReleasesIt() {
|
||||||
val server = SidecarServer(port = 0, service = DriverService(backend = StubDriverBackend("android")))
|
val server = SidecarServer(
|
||||||
|
port = 0,
|
||||||
|
service = DriverService(backend = StubDriverBackend("android")),
|
||||||
|
)
|
||||||
val boundPort = server.start()
|
val boundPort = server.start()
|
||||||
try {
|
try {
|
||||||
assertTrue(boundPort > 0, "expected ephemeral port, got $boundPort")
|
assertTrue(boundPort > 0, "expected ephemeral port, got $boundPort")
|
||||||
|
|||||||
@@ -19,14 +19,22 @@ class SnapshotHandlerTest {
|
|||||||
|
|
||||||
@get:Rule val grpcCleanup: GrpcCleanupRule = GrpcCleanupRule()
|
@get:Rule val grpcCleanup: GrpcCleanupRule = GrpcCleanupRule()
|
||||||
|
|
||||||
private fun newClient(backend: DriverBackend): DriverGrpc.DriverBlockingStub {
|
private fun newClient(
|
||||||
|
backend: DriverBackend,
|
||||||
|
): DriverGrpc.DriverBlockingStub {
|
||||||
val serverName = InProcessServerBuilder.generateName()
|
val serverName = InProcessServerBuilder.generateName()
|
||||||
val service = DriverService(platform = "android", backend = backend)
|
val service = DriverService(platform = "android", backend = backend)
|
||||||
grpcCleanup.register(
|
grpcCleanup.register(
|
||||||
InProcessServerBuilder.forName(serverName).directExecutor().addService(service).build().start(),
|
InProcessServerBuilder.forName(serverName)
|
||||||
|
.directExecutor()
|
||||||
|
.addService(service)
|
||||||
|
.build()
|
||||||
|
.start(),
|
||||||
)
|
)
|
||||||
val channel: ManagedChannel = grpcCleanup.register(
|
val channel: ManagedChannel = grpcCleanup.register(
|
||||||
InProcessChannelBuilder.forName(serverName).directExecutor().build(),
|
InProcessChannelBuilder.forName(serverName)
|
||||||
|
.directExecutor()
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
return DriverGrpc.newBlockingStub(channel)
|
return DriverGrpc.newBlockingStub(channel)
|
||||||
}
|
}
|
||||||
@@ -37,8 +45,10 @@ class SnapshotHandlerTest {
|
|||||||
// forward those calls to the delegate, not these overrides. Override
|
// forward those calls to the delegate, not these overrides. Override
|
||||||
// snapshot() directly so the test exercises the wire path end-to-end.
|
// snapshot() directly so the test exercises the wire path end-to-end.
|
||||||
val backend = object : DriverBackend by StubDriverBackend("android") {
|
val backend = object : DriverBackend by StubDriverBackend("android") {
|
||||||
override fun snapshot(): SnapshotSample =
|
override fun snapshot(): SnapshotSample = SnapshotSample(
|
||||||
SnapshotSample("{\"x\":1}", Triple(byteArrayOf(7, 8, 9), 1080, 2340))
|
"{\"x\":1}",
|
||||||
|
Triple(byteArrayOf(7, 8, 9), 1080, 2340),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|
||||||
@@ -55,13 +65,23 @@ class SnapshotHandlerTest {
|
|||||||
// aligned with the final hierarchy snapshot the runner accepts.
|
// aligned with the final hierarchy snapshot the runner accepts.
|
||||||
val callOrder = mutableListOf<String>()
|
val callOrder = mutableListOf<String>()
|
||||||
val backend = object : DriverBackend {
|
val backend = object : DriverBackend {
|
||||||
override fun launch(bundleId: String, clearState: Boolean, env: Map<String, String>) {}
|
override fun launch(
|
||||||
|
bundleId: String,
|
||||||
|
clearState: Boolean,
|
||||||
|
env: Map<String, String>,
|
||||||
|
) {}
|
||||||
override fun terminate(bundleId: String) {}
|
override fun terminate(bundleId: String) {}
|
||||||
override fun tap(x: Int, y: Int) {}
|
override fun tap(x: Int, y: Int) {}
|
||||||
override fun tapSelector(selector: String) {}
|
override fun tapSelector(selector: String) {}
|
||||||
override fun inputText(text: String) {}
|
override fun inputText(text: String) {}
|
||||||
override fun eraseText(characterCount: Int) {}
|
override fun eraseText(characterCount: Int) {}
|
||||||
override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) {}
|
override fun swipe(
|
||||||
|
fromX: Int,
|
||||||
|
fromY: Int,
|
||||||
|
toX: Int,
|
||||||
|
toY: Int,
|
||||||
|
durationMillis: Long,
|
||||||
|
) {}
|
||||||
override fun pressKey(key: String) {}
|
override fun pressKey(key: String) {}
|
||||||
override fun longPress(x: Int, y: Int) {}
|
override fun longPress(x: Int, y: Int) {}
|
||||||
override fun screenshot(): Triple<ByteArray, Int, Int> {
|
override fun screenshot(): Triple<ByteArray, Int, Int> {
|
||||||
@@ -72,10 +92,14 @@ class SnapshotHandlerTest {
|
|||||||
callOrder.add("hierarchy")
|
callOrder.add("hierarchy")
|
||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> = emptyList()
|
override fun recentLogs(
|
||||||
|
sinceUnixMillis: Long,
|
||||||
|
minLevel: String,
|
||||||
|
): List<LogLine> = emptyList()
|
||||||
override fun waitForIdle(durationMillis: Long) {}
|
override fun waitForIdle(durationMillis: Long) {}
|
||||||
override fun healthy(): Boolean = true
|
override fun healthy(): Boolean = true
|
||||||
override fun metrics(bundleId: String): MetricsSample = MetricsSample(0.0, 0L, 0L)
|
override fun metrics(bundleId: String): MetricsSample =
|
||||||
|
MetricsSample(0.0, 0L, 0L)
|
||||||
}
|
}
|
||||||
backend.snapshot()
|
backend.snapshot()
|
||||||
assertEquals(listOf("hierarchy", "screenshot"), callOrder)
|
assertEquals(listOf("hierarchy", "screenshot"), callOrder)
|
||||||
@@ -88,7 +112,8 @@ class SnapshotHandlerTest {
|
|||||||
val maxObserved = AtomicInteger(0)
|
val maxObserved = AtomicInteger(0)
|
||||||
val callCount = AtomicInteger(0)
|
val callCount = AtomicInteger(0)
|
||||||
val lock = ReentrantLock()
|
val lock = ReentrantLock()
|
||||||
val recordingBackend = object : DriverBackend by StubDriverBackend("android") {
|
val delegate = StubDriverBackend("android")
|
||||||
|
val recordingBackend = object : DriverBackend by delegate {
|
||||||
override fun snapshot(): SnapshotSample {
|
override fun snapshot(): SnapshotSample {
|
||||||
val now = inFlight.incrementAndGet()
|
val now = inFlight.incrementAndGet()
|
||||||
try {
|
try {
|
||||||
@@ -109,9 +134,13 @@ class SnapshotHandlerTest {
|
|||||||
// Use a real (multi-threaded) executor on the server side so the service
|
// Use a real (multi-threaded) executor on the server side so the service
|
||||||
// is not artificially serialized by directExecutor.
|
// is not artificially serialized by directExecutor.
|
||||||
val serverName = InProcessServerBuilder.generateName()
|
val serverName = InProcessServerBuilder.generateName()
|
||||||
val service = DriverService(platform = "android", backend = recordingBackend)
|
val service =
|
||||||
|
DriverService(platform = "android", backend = recordingBackend)
|
||||||
grpcCleanup.register(
|
grpcCleanup.register(
|
||||||
InProcessServerBuilder.forName(serverName).addService(service).build().start(),
|
InProcessServerBuilder.forName(serverName)
|
||||||
|
.addService(service)
|
||||||
|
.build()
|
||||||
|
.start(),
|
||||||
)
|
)
|
||||||
val channel: ManagedChannel = grpcCleanup.register(
|
val channel: ManagedChannel = grpcCleanup.register(
|
||||||
InProcessChannelBuilder.forName(serverName).build(),
|
InProcessChannelBuilder.forName(serverName).build(),
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ class StabilityPollTest {
|
|||||||
elapsed >= MIN_STABLE_STREAK_MILLIS,
|
elapsed >= MIN_STABLE_STREAK_MILLIS,
|
||||||
"must observe a stable streak of at least ${MIN_STABLE_STREAK_MILLIS}ms, elapsed=${elapsed}ms",
|
"must observe a stable streak of at least ${MIN_STABLE_STREAK_MILLIS}ms, elapsed=${elapsed}ms",
|
||||||
)
|
)
|
||||||
assertTrue(elapsed < 3000L, "should not run to cap when stable, elapsed=${elapsed}ms")
|
assertTrue(
|
||||||
|
elapsed < 3000L,
|
||||||
|
"should not run to cap when stable, elapsed=${elapsed}ms",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun slowSnapshotReadsDoNotEatTheStreak() {
|
@Test fun slowSnapshotReadsDoNotEatTheStreak() {
|
||||||
@@ -38,8 +41,9 @@ class StabilityPollTest {
|
|||||||
val observedQuiet = sampleStarts.last() - sampleEnds.first()
|
val observedQuiet = sampleStarts.last() - sampleEnds.first()
|
||||||
assertTrue(
|
assertTrue(
|
||||||
observedQuiet >= MIN_STABLE_STREAK_MILLIS,
|
observedQuiet >= MIN_STABLE_STREAK_MILLIS,
|
||||||
"the poll returned having observed only ${observedQuiet}ms of quiet, not " +
|
"the poll returned having observed only ${observedQuiet}ms of " +
|
||||||
"${MIN_STABLE_STREAK_MILLIS}ms; starts=$sampleStarts ends=$sampleEnds",
|
"quiet, not ${MIN_STABLE_STREAK_MILLIS}ms; " +
|
||||||
|
"starts=$sampleStarts ends=$sampleEnds",
|
||||||
)
|
)
|
||||||
assertTrue(
|
assertTrue(
|
||||||
sampleStarts.size >= 3,
|
sampleStarts.size >= 3,
|
||||||
@@ -60,23 +64,27 @@ class StabilityPollTest {
|
|||||||
calls++
|
calls++
|
||||||
when {
|
when {
|
||||||
calls <= 2 -> "calm"
|
calls <= 2 -> "calm"
|
||||||
|
|
||||||
calls == 3 -> {
|
calls == 3 -> {
|
||||||
transientAt = System.currentTimeMillis()
|
transientAt = System.currentTimeMillis()
|
||||||
"transient"
|
"transient"
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> "stable"
|
else -> "stable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sinceTransition = System.currentTimeMillis() - transientAt
|
val sinceTransition = System.currentTimeMillis() - transientAt
|
||||||
assertTrue(
|
assertTrue(
|
||||||
calls >= 8,
|
calls >= 8,
|
||||||
"after the transition the poll needs a fresh matching pair and then a full " +
|
"after the transition the poll needs a fresh matching pair and " +
|
||||||
"${MIN_STABLE_STREAK_MILLIS}ms of quiet, which is 8 samples, got $calls",
|
"then a full ${MIN_STABLE_STREAK_MILLIS}ms of quiet, which " +
|
||||||
|
"is 8 samples, got $calls",
|
||||||
)
|
)
|
||||||
assertTrue(
|
assertTrue(
|
||||||
sinceTransition >= MIN_STABLE_STREAK_MILLIS,
|
sinceTransition >= MIN_STABLE_STREAK_MILLIS,
|
||||||
"the calm prefix must not count: a full ${MIN_STABLE_STREAK_MILLIS}ms streak has to " +
|
"the calm prefix must not count: a full " +
|
||||||
"start over after the transition, returned ${sinceTransition}ms after it",
|
"${MIN_STABLE_STREAK_MILLIS}ms streak has to start over " +
|
||||||
|
"after the transition, returned ${sinceTransition}ms after it",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +113,10 @@ class StabilityPollTest {
|
|||||||
"frame-$calls"
|
"frame-$calls"
|
||||||
}
|
}
|
||||||
val elapsed = System.currentTimeMillis() - start
|
val elapsed = System.currentTimeMillis() - start
|
||||||
assertTrue(elapsed in budget..(budget + 1000L), "expected to hit cap, elapsed=$elapsed")
|
assertTrue(
|
||||||
|
elapsed in budget..(budget + 1000L),
|
||||||
|
"expected to hit cap, elapsed=$elapsed",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun zeroBudgetReturnsImmediately() {
|
@Test fun zeroBudgetReturnsImmediately() {
|
||||||
@@ -117,7 +128,8 @@ class StabilityPollTest {
|
|||||||
assertEquals(0, calls)
|
assertEquals(0, calls)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun structuralHashIgnoresBoundsAndIdenticalForSemanticallyEqualTrees() {
|
@Test
|
||||||
|
fun structuralHashIgnoresBoundsAndIdenticalForSemanticallyEqualTrees() {
|
||||||
val a = """
|
val a = """
|
||||||
{"attributes":{"resource-id":"LoginScreen","bounds":"[0,0,1080,2340]"},
|
{"attributes":{"resource-id":"LoginScreen","bounds":"[0,0,1080,2340]"},
|
||||||
"children":[
|
"children":[
|
||||||
@@ -130,13 +142,24 @@ class StabilityPollTest {
|
|||||||
{"attributes":{"resource-id":"LoginEmail","bounds":"[10,11,1070,101]","text":"a@b"},"children":[]}
|
{"attributes":{"resource-id":"LoginEmail","bounds":"[10,11,1070,101]","text":"a@b"},"children":[]}
|
||||||
]}
|
]}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
assertEquals(structuralHash(a), structuralHash(b), "bounds-only flicker must not change hash")
|
assertEquals(
|
||||||
|
structuralHash(a),
|
||||||
|
structuralHash(b),
|
||||||
|
"bounds-only flicker must not change hash",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun structuralHashDiffersWhenContentChanges() {
|
@Test fun structuralHashDiffersWhenContentChanges() {
|
||||||
val a = """{"attributes":{"resource-id":"LoginEmail","text":"a@b"},"children":[]}"""
|
val a = """
|
||||||
val b = """{"attributes":{"resource-id":"LoginEmail","text":"c@d"},"children":[]}"""
|
{"attributes":{"resource-id":"LoginEmail","text":"a@b"},"children":[]}
|
||||||
assertTrue(structuralHash(a) != structuralHash(b), "text change must alter hash")
|
""".trimIndent()
|
||||||
|
val b = """
|
||||||
|
{"attributes":{"resource-id":"LoginEmail","text":"c@d"},"children":[]}
|
||||||
|
""".trimIndent()
|
||||||
|
assertTrue(
|
||||||
|
structuralHash(a) != structuralHash(b),
|
||||||
|
"text change must alter hash",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun stabilitySnapshotReturnsNullDuringNavHostCrossFade() {
|
@Test fun stabilitySnapshotReturnsNullDuringNavHostCrossFade() {
|
||||||
@@ -160,7 +183,10 @@ class StabilityPollTest {
|
|||||||
]}
|
]}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val hash = stabilitySnapshot(singleScreen)
|
val hash = stabilitySnapshot(singleScreen)
|
||||||
assertTrue(hash != null && hash.isNotBlank(), "single-screen tree must yield a hash, got $hash")
|
assertTrue(
|
||||||
|
hash != null && hash.isNotBlank(),
|
||||||
|
"single-screen tree must yield a hash, got $hash",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun stabilitySnapshotIgnoresNonRouteAttributeValues() {
|
@Test fun stabilitySnapshotIgnoresNonRouteAttributeValues() {
|
||||||
@@ -173,7 +199,10 @@ class StabilityPollTest {
|
|||||||
{"attributes":{"text":"Welcome to MyScreen"},"children":[]}
|
{"attributes":{"text":"Welcome to MyScreen"},"children":[]}
|
||||||
]}
|
]}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
assertTrue(stabilitySnapshot(tree) != null, "non-route attribute must not be counted as a screen")
|
assertTrue(
|
||||||
|
stabilitySnapshot(tree) != null,
|
||||||
|
"non-route attribute must not be counted as a screen",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun countRouteScreensCountsTestTagAndIdentifier() {
|
@Test fun countRouteScreensCountsTestTagAndIdentifier() {
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ import kotlin.test.assertTrue
|
|||||||
|
|
||||||
class WdaRecoveryTest {
|
class WdaRecoveryTest {
|
||||||
|
|
||||||
private fun recovery(
|
private fun recovery(isAlive: () -> Boolean, restart: () -> Unit) =
|
||||||
isAlive: () -> Boolean,
|
WdaRecovery(isAlive = isAlive, restart = restart, log = {})
|
||||||
restart: () -> Unit,
|
|
||||||
) = WdaRecovery(isAlive = isAlive, restart = restart, log = {})
|
|
||||||
|
|
||||||
@Test fun aliveChannelSkipsRestartAndRetriesReads() {
|
@Test fun aliveChannelSkipsRestartAndRetriesReads() {
|
||||||
val restarts = AtomicInteger(0)
|
val restarts = AtomicInteger(0)
|
||||||
val recovery = recovery(isAlive = { true }, restart = { restarts.incrementAndGet() })
|
val recovery = recovery(
|
||||||
|
isAlive = { true },
|
||||||
|
restart = { restarts.incrementAndGet() },
|
||||||
|
)
|
||||||
var calls = 0
|
var calls = 0
|
||||||
|
|
||||||
val result = recovery.run(replay = true) {
|
val result = recovery.run(replay = true) {
|
||||||
@@ -35,10 +36,15 @@ class WdaRecoveryTest {
|
|||||||
|
|
||||||
@Test fun aliveChannelSurfacesUnavailableForActions() {
|
@Test fun aliveChannelSurfacesUnavailableForActions() {
|
||||||
val restarts = AtomicInteger(0)
|
val restarts = AtomicInteger(0)
|
||||||
val recovery = recovery(isAlive = { true }, restart = { restarts.incrementAndGet() })
|
val recovery = recovery(
|
||||||
|
isAlive = { true },
|
||||||
|
restart = { restarts.incrementAndGet() },
|
||||||
|
)
|
||||||
|
|
||||||
val thrown = assertFailsWith<io.grpc.StatusRuntimeException> {
|
val thrown = assertFailsWith<io.grpc.StatusRuntimeException> {
|
||||||
recovery.run(replay = false) { throw IOException("connection reset") }
|
recovery.run(replay = false) {
|
||||||
|
throw IOException("connection reset")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
|
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
|
||||||
@@ -101,7 +107,9 @@ class WdaRecoveryTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val thrown = assertFailsWith<IllegalStateException> {
|
val thrown = assertFailsWith<IllegalStateException> {
|
||||||
recovery.run(replay = true) { throw IOException("connection refused") }
|
recovery.run(replay = true) {
|
||||||
|
throw IOException("connection refused")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertTrue(thrown.message.orEmpty().contains("WDA reconnect failed"))
|
assertTrue(thrown.message.orEmpty().contains("WDA reconnect failed"))
|
||||||
@@ -116,7 +124,9 @@ class WdaRecoveryTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
assertFailsWith<IllegalArgumentException> {
|
assertFailsWith<IllegalArgumentException> {
|
||||||
recovery.run(replay = true) { throw IllegalArgumentException("bad selector") }
|
recovery.run(replay = true) {
|
||||||
|
throw IllegalArgumentException("bad selector")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(0, restarts.get())
|
assertEquals(0, restarts.get())
|
||||||
@@ -127,7 +137,9 @@ class WdaRecoveryTest {
|
|||||||
val recovery = recovery(isAlive = { true }, restart = {})
|
val recovery = recovery(isAlive = { true }, restart = {})
|
||||||
|
|
||||||
val thrown = assertFailsWith<io.grpc.StatusRuntimeException> {
|
val thrown = assertFailsWith<io.grpc.StatusRuntimeException> {
|
||||||
recovery.run(replay = true) { throw IOException("connection reset") }
|
recovery.run(replay = true) {
|
||||||
|
throw IOException("connection reset")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
|
assertEquals(io.grpc.Status.Code.UNAVAILABLE, thrown.status.code)
|
||||||
|
|||||||
Reference in new issue
Block a user