Skip to content

Commit 9764245

Browse files
committed
Setup Hermes V1 testing
1 parent 8d7e737 commit 9764245

File tree

6 files changed

+599
-2
lines changed

6 files changed

+599
-2
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
name: test-hermes-v1
2+
description: Tests a library on a nightly
3+
inputs:
4+
platform:
5+
description: whether we want to build for iOS or Android
6+
required: true
7+
flavor:
8+
required: true
9+
description: the flavor we want to run - either debug or release
10+
retry-count:
11+
description: Number of retry attempts for failed builds
12+
required: false
13+
default: '3'
14+
runs:
15+
using: composite
16+
steps:
17+
- name: Prepare capitalized flavor
18+
id: prepare-flavor
19+
shell: bash
20+
run: |
21+
CAPITALIZED_FLAVOR=$(echo "${{ inputs.flavor }}" | awk '{print toupper(substr($0, 1, 1)) substr($0, 2)}')
22+
echo "capitalized_flavor=$CAPITALIZED_FLAVOR" >> $GITHUB_OUTPUT
23+
24+
- name: Setup Java
25+
uses: actions/setup-java@v2
26+
with:
27+
java-version: '17'
28+
distribution: 'zulu'
29+
30+
- name: Install Maestro
31+
shell: bash
32+
run: export MAESTRO_VERSION=1.40.0; curl -Ls "https://get.maestro.mobile.dev" | bash
33+
34+
- name: Install Maestro dependencies
35+
if: ${{ inputs.platform == 'ios' }}
36+
shell: bash
37+
run: |
38+
brew tap facebook/fb
39+
brew install facebook/fb/idb-companion
40+
41+
- name: Create new app
42+
shell: bash
43+
run: |
44+
cd /tmp
45+
npx @react-native-community/cli init RNApp --skip-install --version nightly
46+
47+
- name: Apply patch if specified
48+
shell: bash
49+
run: |
50+
if [ -f "$GITHUB_WORKSPACE/patches/hermes-v1.patch" ]; then
51+
echo "Applying patch: hermes-v1.patch"
52+
cd /tmp/RNApp
53+
node "$GITHUB_WORKSPACE/scripts/apply-patch.js" "$GITHUB_WORKSPACE/patches/hermes-v1.patch"
54+
echo "✅ Patch applied successfully"
55+
else
56+
echo "⚠️ Warning: Patch file not found: patches/hermes-v1.patch"
57+
exit 1
58+
fi
59+
60+
- name: Install app dependencies with retry
61+
uses: nick-fields/retry@v3
62+
with:
63+
timeout_minutes: 10
64+
max_attempts: ${{ inputs.retry-count }}
65+
retry_wait_seconds: 15
66+
shell: bash
67+
command: |
68+
cd /tmp/RNApp
69+
yarn install
70+
on_retry_command: |
71+
echo "Cleaning up for yarn retry..."
72+
cd /tmp/RNApp
73+
rm -rf node_modules yarn.lock || true
74+
yarn cache clean || true
75+
76+
# iOS
77+
- name: Setup xcode
78+
if: ${{ inputs.platform == 'ios' }}
79+
uses: maxim-lobanov/setup-xcode@v1
80+
with:
81+
xcode-version: 16.4.0
82+
- name: Build iOS with retry
83+
if: ${{ inputs.platform == 'ios' }}
84+
uses: nick-fields/retry@v3
85+
with:
86+
timeout_minutes: 45
87+
max_attempts: ${{ inputs.retry-count }}
88+
retry_wait_seconds: 30
89+
shell: bash
90+
command: |
91+
cd /tmp/RNApp/ios
92+
bundle install
93+
RCT_HERMES_V1_ENABLED=1 bundle exec pod install
94+
xcodebuild build \
95+
-workspace "RNApp.xcworkspace" \
96+
-scheme "RNApp" \
97+
-configuration "${{ steps.prepare-flavor.outputs.capitalized_flavor }}" \
98+
-sdk "iphonesimulator" \
99+
-destination "generic/platform=iOS Simulator" \
100+
-derivedDataPath "/tmp/RNApp" \
101+
-quiet
102+
on_retry_command: |
103+
echo "Cleaning up for iOS retry..."
104+
cd /tmp/RNApp/ios
105+
rm -rf Pods Podfile.lock build
106+
rm -rf ~/Library/Developer/Xcode/DerivedData/* || true
107+
108+
# Android
109+
- name: Build Android with retry
110+
if: ${{ inputs.platform == 'android' }}
111+
uses: nick-fields/retry@v3
112+
env:
113+
CMAKE_VERSION: 3.31.5
114+
ORG_GRADLE_PROJECT_reactNativeArchitectures: x86_64
115+
with:
116+
timeout_minutes: 45
117+
max_attempts: ${{ inputs.retry-count }}
118+
retry_wait_seconds: 30
119+
shell: bash
120+
command: |
121+
cd /tmp/RNApp/android
122+
./gradlew assemble${{ steps.prepare-flavor.outputs.capitalized_flavor }}
123+
on_retry_command: |
124+
echo "Cleaning up for Android retry..."
125+
cd /tmp/RNApp/android
126+
./gradlew clean || true
127+
rm -rf build app/build .gradle || true
128+
129+
- name: Enable KVM group perms
130+
if: ${{ inputs.platform == 'android' }}
131+
shell: bash
132+
run: |
133+
# ubuntu machines have hardware acceleration available and when we try to create an emulator, the script pauses asking for user input
134+
# These lines set the rules to reply automatically to that question and unblock the creation of the emulator.
135+
# source: https://github.com/ReactiveCircus/android-emulator-runner?tab=readme-ov-file#running-hardware-accelerated-emulators-on-linux-runners
136+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
137+
sudo udevadm control --reload-rules
138+
sudo udevadm trigger --name-match=kvm
139+
140+
- name: Start Metro in debug
141+
if: ${{ inputs.flavor == 'debug' }}
142+
shell: bash
143+
run: |
144+
cd /tmp/RNApp
145+
yarn start &
146+
echo $! > /tmp/metro.pid
147+
sleep 5
148+
149+
- name: Run iOS E2E Tests
150+
if: ${{ inputs.platform == 'ios' }}
151+
id: run-tests-ios
152+
shell: bash
153+
run: |
154+
node .github/workflow-scripts/maestro-ios.js \
155+
"/tmp/RNApp/Build/Products/${{ steps.prepare-flavor.outputs.capitalized_flavor }}-iphonesimulator/RNApp.app" \
156+
"org.reactjs.native.example.RNApp" \
157+
"./.github/workflow-scripts/e2e/.maestro/" \
158+
"Hermes" \
159+
"${{ steps.prepare-flavor.outputs.capitalized_flavor }}" \
160+
"/tmp/RNApp"
161+
162+
- name: Store iOS tests result
163+
if: ${{ inputs.platform == 'ios' && (success() || failure()) }}
164+
uses: actions/upload-artifact@v4.3.4
165+
with:
166+
name: e2e_ios_hermes_v1_report_${{ inputs.flavor }}
167+
path: |
168+
video_record_1.mov
169+
video_record_2.mov
170+
video_record_3.mov
171+
video_record_4.mov
172+
video_record_5.mov
173+
report.xml
174+
175+
- name: Run Android E2E tests
176+
if: ${{ inputs.platform == 'android' }}
177+
id: run-tests-android
178+
uses: reactivecircus/android-emulator-runner@v2
179+
with:
180+
api-level: 24
181+
arch: x86_64
182+
ram-size: '8192M'
183+
heap-size: '4096M'
184+
disk-size: '6G'
185+
cores: '4'
186+
disable-animations: false
187+
avd-name: e2e_emulator
188+
script: |
189+
node .github/workflow-scripts/maestro-android.js /tmp/RNApp/android/app/build/outputs/apk/${{ inputs.flavor }}/app-${{ inputs.flavor }}.apk com.rnapp ./.github/workflow-scripts/e2e/.maestro/ ${{ inputs.flavor }} /tmp/RNApp
190+
191+
- name: Store Android tests result
192+
uses: actions/upload-artifact@v4.3.4
193+
if: ${{ inputs.platform == 'android' && (success() || failure()) }}
194+
with:
195+
name: e2e_android_hermes_v1_report_${{ inputs.flavor }}
196+
path: |
197+
report.xml
198+
screen.mp4
199+
200+
- name: Stop Metro in debug
201+
if: ${{ inputs.flavor == 'debug' && always() }}
202+
shell: bash
203+
run: |
204+
METRO_PID=$(cat /tmp/metro.pid)
205+
kill $METRO_PID || true
206+
echo "Metro process $METRO_PID stopped."
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
appId: ${APP_ID} # iOS: org.reactjs.native.example.RNApp | Android: com.rnapp
2+
---
3+
- launchApp
4+
- assertVisible: "Welcome to React Native"
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const childProcess = require('child_process');
11+
const fs = require('fs');
12+
13+
const usage = `
14+
=== Usage ===
15+
node maestro-android.js <path to app> <app_id> <maestro_flow> <flavor> <working_directory>
16+
17+
@param {string} appPath - Path to the app APK
18+
@param {string} appId - App ID that needs to be launched
19+
@param {string} maestroFlow - Path to the maestro flow to be executed
20+
@param {string} flavor - Flavor of the app to be launched. Can be 'release' or 'debug'
21+
@param {string} workingDirectory - Working directory from where to run Metro
22+
==============
23+
`;
24+
25+
const args = process.argv.slice(2);
26+
27+
if (args.length !== 5) {
28+
throw new Error(`Invalid number of arguments.\n${usage}`);
29+
}
30+
31+
const APP_PATH = args[0];
32+
const APP_ID = args[1];
33+
const MAESTRO_FLOW = args[2];
34+
const IS_DEBUG = args[3] === 'debug';
35+
const WORKING_DIRECTORY = args[4];
36+
37+
const MAX_ATTEMPTS = 3;
38+
39+
async function executeFlowWithRetries(flow, currentAttempt) {
40+
try {
41+
console.info(`Executing flow: ${flow}`);
42+
const timeout = 1000 * 60 * 10; // 10 minutes
43+
childProcess.execSync(
44+
`MAESTRO_DRIVER_STARTUP_TIMEOUT=120000 $HOME/.maestro/bin/maestro test ${flow} --format junit -e APP_ID=${APP_ID} --debug-output /tmp/MaestroLogs`,
45+
{stdio: 'inherit', timeout},
46+
);
47+
} catch (err) {
48+
if (currentAttempt < MAX_ATTEMPTS) {
49+
console.info(`Retrying...`);
50+
await executeFlowWithRetries(flow, currentAttempt + 1);
51+
} else {
52+
throw err;
53+
}
54+
}
55+
}
56+
57+
async function executeFlowInFolder(flowFolder) {
58+
const files = fs.readdirSync(flowFolder);
59+
for (const file of files) {
60+
const filePath = `${flowFolder}/${file}`;
61+
if (fs.lstatSync(filePath).isDirectory()) {
62+
await executeFlowInFolder(filePath);
63+
} else {
64+
await executeFlowWithRetries(filePath, 0);
65+
}
66+
}
67+
}
68+
69+
async function main() {
70+
console.info('\n==============================');
71+
console.info('Running tests for Android with the following parameters:');
72+
console.info(`APP_PATH: ${APP_PATH}`);
73+
console.info(`APP_ID: ${APP_ID}`);
74+
console.info(`MAESTRO_FLOW: ${MAESTRO_FLOW}`);
75+
console.info(`IS_DEBUG: ${IS_DEBUG}`);
76+
console.info(`WORKING_DIRECTORY: ${WORKING_DIRECTORY}`);
77+
console.info('==============================\n');
78+
79+
console.info('Install app');
80+
childProcess.execSync(`adb install ${APP_PATH}`, {stdio: 'ignore'});
81+
82+
console.info('Start the app');
83+
childProcess.execSync(`adb shell monkey -p ${APP_ID} 1`, {stdio: 'ignore'});
84+
85+
if (IS_DEBUG) {
86+
console.info('Wait For App to warm from Metro');
87+
await sleep(25000);
88+
}
89+
90+
console.info('Start recording to /sdcard/screen.mp4');
91+
childProcess
92+
.exec('adb shell screenrecord /sdcard/screen.mp4', {
93+
stdio: 'ignore',
94+
detached: true,
95+
})
96+
.unref();
97+
98+
console.info(`Start testing ${MAESTRO_FLOW}`);
99+
let error = null;
100+
try {
101+
//check if MAESTRO_FLOW is a folder
102+
if (
103+
fs.existsSync(MAESTRO_FLOW) &&
104+
fs.lstatSync(MAESTRO_FLOW).isDirectory()
105+
) {
106+
await executeFlowInFolder(MAESTRO_FLOW);
107+
} else {
108+
await executeFlowWithRetries(MAESTRO_FLOW, 0);
109+
}
110+
} catch (err) {
111+
error = err;
112+
} finally {
113+
console.info('Stop recording');
114+
childProcess.execSync('adb pull /sdcard/screen.mp4', {stdio: 'ignore'});
115+
}
116+
117+
if (error) {
118+
throw error;
119+
}
120+
process.exit();
121+
}
122+
123+
function sleep(ms) {
124+
return new Promise(resolve => {
125+
setTimeout(resolve, ms);
126+
});
127+
}
128+
129+
main();

0 commit comments

Comments
 (0)