Skip to content

Commit f7ee904

Browse files
authored
(CI) Switch to Google ARM emulators for SDK35 (#4814)
1 parent 69e708a commit f7ee904

File tree

12 files changed

+197
-36
lines changed

12 files changed

+197
-36
lines changed

.buildkite/jobs/pipeline.android_rn_79.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
env:
66
REACT_NATIVE_VERSION: 0.79.5
77
RCT_NEW_ARCH_ENABLED: 1
8-
USE_GENYCLOUD_ARM64: true # Scarce resource: Use this only for the latest RN version
8+
USE_GOOGLE_ARM64: true
99
DETOX_DISABLE_POD_INSTALL: true
1010
DETOX_DISABLE_POSTINSTALL: true
1111
artifact_paths:

detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/DetoxIdlingResource.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ abstract class DetoxIdlingResource : DescriptiveIdlingResource {
1919
paused.set(false)
2020
}
2121

22-
2322
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
2423
this.callback = callback
2524
}
@@ -37,7 +36,6 @@ abstract class DetoxIdlingResource : DescriptiveIdlingResource {
3736

3837
protected abstract fun checkIdle(): Boolean
3938

40-
4139
fun notifyIdle() {
4240
callback?.onTransitionToIdle()
4341
}

detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/ReactNativeIdlingResources.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource
1616
import kotlinx.coroutines.runBlocking
1717
import org.joor.Reflect
1818

19-
2019
private const val LOG_TAG = "DetoxRNIdleRes"
2120

2221
class ReactNativeIdlingResources(
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.wix.detox.reactnative.idlingresources
2+
3+
import androidx.test.espresso.IdlingResource.ResourceCallback
4+
/**
5+
* A wrapper for idling resources that exhibit "flapping" behavior.
6+
* This wrapper artificially stabilizes them by mindfully extending their "busy" state periods.
7+
*
8+
* #### What does flapping mean?
9+
*
10+
* Some idling resources have extremely short busy periods. While this shouldn't theoretically be an issue,
11+
* in real-world scenarios (e.g., a React Native environment), the number of idling resources can accumulate.
12+
* Unfortunately, the process of checking if all resources are idle (idle interrogation)
13+
* isn't atomic. With many resources involved, this process might miss those brief busy periods
14+
* of resources that "flap" (quickly switch between busy and idle) during the interrogation.
15+
*
16+
* #### How does this wrapper fix flapping?
17+
*
18+
* This wrapper requires the wrapped resource to report itself as "idle" multiple times consecutively
19+
* before the wrapper itself is considered idle. This dramatically reduces the likelihood of it's busy phase being
20+
* missed by the idle interrogation process.
21+
*/
22+
class StabilizedIdlingResource(
23+
private val idlingResource: DetoxIdlingResource,
24+
private val size: Int): DetoxIdlingResource(), ResourceCallback {
25+
26+
private val name = "${idlingResource.name} (stable@$size)"
27+
private var idleCounter = 0
28+
29+
init {
30+
if (size <= 1) {
31+
throw IllegalArgumentException("Size must be > 1 in order for the usage of this class to make sense")
32+
}
33+
idlingResource.registerIdleTransitionCallback(this)
34+
}
35+
36+
/**
37+
* Implemented according to these 3 guiding principles:
38+
* 1. As long as the actual resource is busy - we consider ourselves busy too.
39+
* 2. Once actual resource is idle enough times in a row - we consider ourselves "idle", in a *sticky* way
40+
* (until it returns "busy" in an interrogation).
41+
* 3. Espresso requires that just before we officially transition ourselves -- idle -> busy, we actively notify it
42+
* via the callback.
43+
*/
44+
@Synchronized
45+
override fun checkIdle(): Boolean {
46+
if (!idlingResource.isIdleNow) {
47+
idleCounter = 0
48+
return false
49+
}
50+
51+
if (idleCounter < size) {
52+
idleCounter++
53+
54+
if (idleCounter == size) {
55+
notifyIdle()
56+
}
57+
}
58+
return (idleCounter == size)
59+
}
60+
61+
/**
62+
* Resets the counter to 0, assuming that Espresso will re-query this (among all other) resources
63+
* immediately after this "I'm idle" notification.
64+
*/
65+
@Synchronized
66+
override fun onTransitionToIdle() {
67+
idleCounter = 0
68+
notifyIdle()
69+
}
70+
71+
override fun getName(): String = name
72+
override fun getDebugName(): String = idlingResource.getDebugName()
73+
override fun getBusyHint(): Map<String, Any>? = idlingResource.getBusyHint()
74+
}

detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/factory/FabricDetoxIdlingResourceFactoryStrategy.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.wix.detox.reactnative.idlingresources.factory
22

33
import com.facebook.react.ReactApplication
4-
import com.wix.detox.reactnative.getCurrentReactContext
54
import com.wix.detox.reactnative.getCurrentReactContextSafe
65
import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource
6+
import com.wix.detox.reactnative.idlingresources.StabilizedIdlingResource
77
import com.wix.detox.reactnative.idlingresources.animations.AnimatedModuleIdlingResource
88
import com.wix.detox.reactnative.idlingresources.network.NetworkIdlingResource
99
import com.wix.detox.reactnative.idlingresources.storage.AsyncStorageIdlingResource
@@ -23,7 +23,7 @@ class FabricDetoxIdlingResourceFactoryStrategy(private val reactApplication: Rea
2323
IdlingResourcesName.Animations to AnimatedModuleIdlingResource(reactContext),
2424
IdlingResourcesName.Timers to FabricTimersIdlingResource(reactContext),
2525
IdlingResourcesName.Network to NetworkIdlingResource(reactContext),
26-
IdlingResourcesName.AsyncStorage to AsyncStorageIdlingResource(reactContext)
26+
IdlingResourcesName.AsyncStorage to StabilizedIdlingResource(AsyncStorageIdlingResource(reactContext), 2)
2727
)
2828

2929
return@withContext result

detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/storage/AsyncStorageIdlingResource.kt

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.wix.detox.reactnative.idlingresources.storage
22

3-
import android.util.Log
43
import androidx.test.espresso.IdlingResource
54
import com.facebook.react.bridge.NativeModule
65
import com.facebook.react.bridge.ReactContext
@@ -9,36 +8,17 @@ import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource
98
import org.joor.Reflect
109
import java.util.concurrent.Executor
1110

12-
private const val LOG_TAG = "AsyncStorageIR"
13-
1411
private typealias SExecutorReflectedGenFnType = (executor: Executor) -> SerialExecutorReflected
1512

1613
private val defaultSExecutorReflectedGenFn: SExecutorReflectedGenFnType =
1714
{ executor: Executor -> SerialExecutorReflected(executor) }
1815

19-
private class ModuleReflected(module: NativeModule, sexecutorReflectedGen: SExecutorReflectedGenFnType) {
20-
private val executorReflected: SerialExecutorReflected
21-
22-
init {
23-
val reflected = Reflect.on(module)
24-
val executor: Executor = reflected.field("executor").get()
25-
executorReflected = sexecutorReflectedGen(executor)
26-
}
27-
28-
val sexecutor: SerialExecutorReflected
29-
get() = executorReflected
30-
}
31-
32-
class AsyncStorageIdlingResource
33-
@JvmOverloads constructor(
16+
class AsyncStorageIdlingResource @JvmOverloads constructor(
3417
private val reactContext: ReactContext,
3518
private val sexecutorReflectedGenFn: SExecutorReflectedGenFnType = defaultSExecutorReflectedGenFn,
36-
private val rnHelpers: RNHelpers = RNHelpers()
19+
private val rnHelpers: RNHelpers = RNHelpers(),
3720
) : DetoxIdlingResource() {
3821

39-
val logTag: String
40-
get() = LOG_TAG
41-
4222
private val moduleReflected: ModuleReflected? = null
4323
private var idleCheckTask: Runnable? = null
4424
private val idleCheckTaskImpl = Runnable {
@@ -86,13 +66,14 @@ class AsyncStorageIdlingResource
8666

8767
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
8868
super.registerIdleTransitionCallback(callback)
69+
70+
// TODO Don't do?
8971
enqueueIdleCheckTask()
9072
}
9173

9274
override fun checkIdle(): Boolean =
9375
checkIdleInternal().also { idle ->
9476
if (!idle) {
95-
Log.d(logTag, "Async-storage is busy!")
9677
enqueueIdleCheckTask()
9778
}
9879
}
@@ -117,3 +98,16 @@ class AsyncStorageIdlingResource
11798
idleCheckTask = null
11899
}
119100
}
101+
102+
private class ModuleReflected(module: NativeModule, sexecutorReflectedGen: SExecutorReflectedGenFnType) {
103+
private val executorReflected: SerialExecutorReflected
104+
105+
init {
106+
val reflected = Reflect.on(module)
107+
val executor: Executor = reflected.field("executor").get()
108+
executorReflected = sexecutorReflectedGen(executor)
109+
}
110+
111+
val sexecutor: SerialExecutorReflected
112+
get() = executorReflected
113+
}

detox/test/e2e/detox.config.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,8 @@ const config = {
106106
'android.emulator': {
107107
type: 'android.emulator',
108108
headless: Boolean(process.env.CI),
109-
gpuMode: process.env.CI ? 'off' : undefined,
110109
device: {
111-
avdName: 'Pixel_3a_API_34'
110+
avdName: 'Pixel_3a_API_35'
112111
},
113112
utilBinaryPaths: ["e2e/util-binary/detoxbutler-1.0.4-aosp-release.apk"]
114113
},
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const deviceInitCache = new Set();
2+
3+
module.exports = {
4+
isInitialized: (deviceId) => deviceInitCache.has(deviceId),
5+
markInitialized: (deviceId) => deviceInitCache.add(deviceId),
6+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const { device } = require('detox');
2+
3+
const initCache = require('./DeviceInitCache');
4+
const sleep = require('../../utils/sleep');
5+
const log = {
6+
info: (...args) => console.log('[AndroidUIInit]', ...args),
7+
}
8+
const adbWrapper = () => {
9+
const adbName = device.id;
10+
const { adb } = device.deviceDriver;
11+
12+
const shell = async (cmd) => {
13+
await adb.shell(adbName, cmd);
14+
await sleep(200);
15+
};
16+
17+
return {
18+
name: adbName,
19+
shell,
20+
};
21+
};
22+
23+
class AndroidUIInitHelper {
24+
subscribe({ testEvents }) {
25+
testEvents.on('setup', this._handleSetupEvent.bind(this));
26+
}
27+
28+
async _handleSetupEvent() {
29+
if (device.getPlatform() !== 'android') {
30+
return;
31+
}
32+
33+
34+
const adb = adbWrapper();
35+
36+
if (initCache.isInitialized(adb.name)) {
37+
log.info(`Skipping setup for ${adb.name} (already initialized by this worker)`);
38+
return;
39+
}
40+
41+
log.info(`Running init for ${adb.name}`);
42+
43+
await this._setupKeyboardBehavior(adb);
44+
await this._setupPointerIndicators(adb);
45+
await this._setupNavigationMode(adb);
46+
await this._setupStatusBar(adb);
47+
48+
initCache.markInitialized(adb.name);
49+
log.info(`Finished init for ${adb.name}`);
50+
}
51+
52+
async _setupKeyboardBehavior(adb) {
53+
// Force-hide the on-screen keyboard
54+
await adb.shell('settings put Secure show_ime_with_hard_keyboard 0');
55+
}
56+
57+
async _setupPointerIndicators(adb) {
58+
await adb.shell('settings put system show_touches 1');
59+
await adb.shell('settings put system pointer_location 1');
60+
}
61+
62+
async _setupNavigationMode(adb) {
63+
await adb.shell('cmd overlay enable com.android.internal.systemui.navbar.threebutton');
64+
}
65+
66+
// Ref: https://android.googlesource.com/platform/frameworks/base/+/master/packages/SystemUI/docs/demo_mode.md
67+
async _setupStatusBar(adb) {
68+
// Enable, then get out (= reset status-bar) and back into demo mode
69+
await adb.shell('settings put global sysui_demo_allowed 1');
70+
await adb.shell('am broadcast -a com.android.systemui.demo -e command exit');
71+
await adb.shell('am broadcast -a com.android.systemui.demo -e command enter');
72+
73+
// Force status bar content
74+
await adb.shell('am broadcast -a com.android.systemui.demo -e command notifications -e visible false');
75+
await adb.shell('am broadcast -a com.android.systemui.demo -e command network -e wifi hide');
76+
await adb.shell('am broadcast -a com.android.systemui.demo -e command network -e wifi show -e level 4 -e fully true');
77+
await adb.shell('am broadcast -a com.android.systemui.demo -e command network -e mobile hide');
78+
// Best to keep this last due to a "charging" indicator animation which can
79+
// break UI changes made by consequent commands
80+
await adb.shell('am broadcast -a com.android.systemui.demo -e command battery -e level 100 -e plugged true');
81+
await sleep(1500); // Wait for the animation to finish
82+
}
83+
}
84+
85+
module.exports = new AndroidUIInitHelper();

detox/test/e2e/testEnvironment.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
const { DetoxCircusEnvironment } = require('detox/runners/jest');
22
const { worker } = require('detox/internals')
3+
const AndroidUIInitHelper = require('./helpers/AndroidUIInit');
34

45
class CustomDetoxEnvironment extends DetoxCircusEnvironment {
6+
constructor(...args) {
7+
super(...args);
8+
9+
AndroidUIInitHelper.subscribe({ testEvents: this.testEvents});
10+
}
11+
512
async setup() {
613
await super.setup();
714

0 commit comments

Comments
 (0)