Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions .github/workflows/emulator-smoke.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
name: Emulator smoke

# Opt-in / scheduled lane: boots the app on a Gradle Managed Device and runs
# the instrumented LaunchSmokeTest (RESUMED + stability + composited pixels).
# LaunchSmokeTest (RESUMED + stability + composited pixels) and the credential-free
# stale OAuth callback regression. Other device suites require explicit opt-in.
# Not a PR gate — emulator boots on shared runners are too slow and flaky to
# block merges; the PR gate is the CI workflow. Local day-to-day proof stays
# on scripts/prove.sh.
Expand All @@ -20,7 +21,7 @@ concurrency:

jobs:
smoke:
name: Launch smoke (mobile, managed device)
name: Launch and OAuth smoke (mobile, managed device)
runs-on: ubuntu-24.04
timeout-minutes: 60

Expand Down Expand Up @@ -82,12 +83,31 @@ jobs:
working-directory: putio-android
run: >-
./gradlew :app:ciPhoneMobileProductionDebugAndroidTest
-Pandroid.testInstrumentationRunnerArguments.class=io.putdotio.android.LaunchSmokeTest
-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader

- name: Upload device test results
- name: Upload launch test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-device-results
name: launch-smoke-results
if-no-files-found: warn
path: putio-android/app/build/outputs/androidTest-results/**

# AGP's instrumentation-args map drops comma-separated class selectors.
# Keep the OAuth regression explicit and retain each run's result files.
- name: Run stale OAuth callback regression
id: oauth-smoke
working-directory: putio-android
run: >-
./gradlew :app:ciPhoneMobileProductionDebugAndroidTest
-Pandroid.testInstrumentationRunnerArguments.class=io.putdotio.android.auth.StaleOAuthCallbackTest
-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader

- name: Upload OAuth test results
if: ${{ !cancelled() && steps.oauth-smoke.outcome != 'skipped' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: oauth-smoke-results
if-no-files-found: warn
path: putio-android/app/build/outputs/androidTest-results/**
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,11 @@ worktree's ignored `local.properties` before running Gradle. Include local
modifications with a failure report; SHA pairs describe only committed source.

Emulator-on-CI: `.github/workflows/emulator-smoke.yml` (weekly schedule +
`workflow_dispatch`) runs `LaunchSmokeTest` on the `ciPhone` Gradle Managed
Device (`app/build.gradle.kts`, API 36, swiftshader) with KVM enabled on the
runner. It is deliberately not a PR gate: shared-runner emulator boots are too
`workflow_dispatch`) explicitly selects `LaunchSmokeTest` and the credential-free
`StaleOAuthCallbackTest` in separate invocations with separate result artifacts;
opt-in feature suites remain separate. Both run on the
`ciPhone` Gradle Managed Device (`app/build.gradle.kts`, API 36, swiftshader) with
KVM enabled on the runner. It is deliberately not a PR gate: shared-runner emulator boots are too
slow and flaky to block merges, so `scripts/prove.sh` stays the local proof.

## Harness
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -57,22 +56,22 @@ class StaleOAuthCallbackTest {
val newerAttempt = requireNotNull(pendingStore.read())
assertNotEquals(olderAttempt.state, newerAttempt.state)

runtime.dispatchAuthTabResult(
val staleCallback = runtime.dispatchAuthTabResult(
AuthTabIntent.RESULT_OK,
"putio://auth#state=${olderAttempt.state}&access_token=synthetic-stale-token",
)
withTimeout(CALLBACK_TIMEOUT_MILLIS) { runtimeJob.children.toList().joinAll() }
withTimeout(CALLBACK_TIMEOUT_MILLIS) { staleCallback.join() }

assertNull(runtimeFailure.get())
assertEquals(newerAttempt, pendingStore.read())
assertEquals(MobileAuthState.AwaitingOAuthCallback, controller.state.value)
assertNull(tokenStore.read())

runtime.dispatchAuthTabResult(
val matchingCallback = runtime.dispatchAuthTabResult(
AuthTabIntent.RESULT_OK,
"putio://auth#state=${newerAttempt.state}&access_token=",
)
withTimeout(CALLBACK_TIMEOUT_MILLIS) { runtimeJob.children.toList().joinAll() }
withTimeout(CALLBACK_TIMEOUT_MILLIS) { matchingCallback.join() }

assertNull(runtimeFailure.get())
assertNull(pendingStore.read())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.putdotio.sdk.PutioConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -49,7 +50,7 @@ class MobileOAuthRuntime internal constructor(
fun dispatchAuthTabResult(
resultCode: Int,
rawResultUri: String?,
) {
): Job =
// Activity results outlive individual UI owners; keep a boundary failure inside this application scope.
@Suppress("TooGenericExceptionCaught")
applicationScope.launch {
Expand All @@ -65,7 +66,6 @@ class MobileOAuthRuntime internal constructor(
failureReporter.report(error)
}
}
}

companion object {
@Volatile
Expand Down
Loading