Skip to content

Commit

Permalink
Move E2E scripts to js (#48419)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #48419

This change moves the E2E scripts for iOS to a JS script.
This should make it much easier to modify the code in case we need to change it.

## Changelog
[Internal] - Move e2e script from bash to JS

Reviewed By: cortinico

Differential Revision: D67737950

fbshipit-source-id: d0b0411c8a4d688c10e460e70b11dbfc83aaa135
  • Loading branch information
cipolleschi authored and facebook-github-bot committed Dec 31, 2024
1 parent 09995fc commit 93117ea
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 46 deletions.
53 changes: 7 additions & 46 deletions .github/actions/maestro-ios/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,52 +57,13 @@ runs:
# Maestro can fail in case of flakyness, we have some retry logic.
set +e
echo "Launching iOS Simulator: iPhone 15 Pro"
xcrun simctl boot "iPhone 15 Pro"
echo "Installing app on Simulator"
xcrun simctl install booted "${{ inputs.app-path }}"
echo "Retrieving device UDID"
UDID=$(xcrun simctl list devices booted -j | jq -r '[.devices[]] | add | first | .udid')
echo "UDID is $UDID"
echo "Bring simulator in foreground"
open -a simulator
echo "Launch the app"
xcrun simctl launch $UDID ${{ inputs.app-id }}
if [[ ${{ inputs.flavor }} == 'Debug' ]]; then
# To give the app time to warm the metro's cache
sleep 20
fi
echo "Running tests with Maestro"
export MAESTRO_DRIVER_STARTUP_TIMEOUT=1500000 # 25 min. CI is extremely slow
# Add retries for flakyness
MAX_ATTEMPTS=5
CURR_ATTEMPT=0
RESULT=1
while [[ $CURR_ATTEMPT -lt $MAX_ATTEMPTS ]] && [[ $RESULT -ne 0 ]]; do
CURR_ATTEMPT=$((CURR_ATTEMPT+1))
echo "Attempt number $CURR_ATTEMPT"
echo "Start video record using pid: video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid"
xcrun simctl io booted recordVideo video_record_$CURR_ATTEMPT.mov & echo $! > video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid
echo '$HOME/.maestro/bin/maestro --udid=$UDID test ${{ inputs.maestro-flow }} --format junit -e APP_ID=${{ inputs.app-id }}'
$HOME/.maestro/bin/maestro --udid=$UDID test ${{ inputs.maestro-flow }} --format junit -e APP_ID=${{ inputs.app-id }} --debug-output /tmp/MaestroLogs
RESULT=$?
# Stop video
kill -SIGINT $(cat video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid)
done
exit $RESULT
node .github/workflow-scripts/maestro-ios.js \
"${{ inputs.app-path }}" \
"${{ inputs.app-id }}" \
"${{ inputs.maestro-flow }}" \
"${{ inputs.jsengine }}" \
"${{ inputs.flavor }}" \
"${{ inputs.working-directory }}"
- name: Store video record
if: always()
uses: actions/[email protected]
Expand Down
167 changes: 167 additions & 0 deletions .github/workflow-scripts/maestro-ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

const childProcess = require('child_process');
const fs = require('fs');

const usage = `
=== Usage ===
node maestro-android.js <path to app> <app_id> <maestro_flow> <flavor> <working_directory>
@param {string} appPath - Path to the app APK
@param {string} appId - App ID that needs to be launched
@param {string} maestroFlow - Path to the maestro flow to be executed
@param {string} jsengine - The JSEngine to use for the test
@param {string} flavor - Flavor of the app to be launched. Can be 'Release' or 'Debug'
@param {string} workingDirectory - Working directory from where to run Metro
==============
`;

const args = process.argv.slice(2);

if (args.length !== 6) {
throw new Error(`Invalid number of arguments.\n${usage}`);
}

const APP_PATH = args[0];
const APP_ID = args[1];
const MAESTRO_FLOW = args[2];
const JS_ENGINE = args[3];
const IS_DEBUG = args[4] === 'Debug';
const WORKING_DIRECTORY = args[5];

const MAX_ATTEMPTS = 5;

function launchSimulator(simulatorName) {
console.log(`Launching simulator ${simulatorName}`);
try {
childProcess.execSync(`xcrun simctl boot "${simulatorName}"`);
} catch (error) {
if (
!error.message.includes('Unable to boot device in current state: Booted')
) {
throw error;
}
}
}

function installAppOnSimulator(appPath) {
console.log(`Installing app at path ${appPath}`);
childProcess.execSync(`xcrun simctl install booted "${appPath}"`);
}

function extractSimulatorUDID() {
console.log('Retrieving device UDID');
const command = `xcrun simctl list devices booted -j | jq -r '[.devices[]] | add | first | .udid'`;
const udid = String(childProcess.execSync(command)).trim();
console.log(`UDID is ${udid}`);
return udid;
}

function bringSimulatorInForeground() {
console.log('Bringing simulator in foreground');
childProcess.execSync('open -a simulator');
}

function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}

async function launchAppOnSimulator(appId, udid, isDebug) {
console.log('Launch the app');
childProcess.execSync(`xcrun simctl launch "${udid}" "${appId}"`);

if (isDebug) {
console.log('Wait for metro to warm');
await sleep(20 * 1000);
}
}

function startVideoRecording(jsengine, currentAttempt) {
console.log(
`Start video record using pid: video_record_${jsengine}_${currentAttempt}.pid`,
);
childProcess.exec(
`xcrun simctl io booted recordVideo video_record_${jsengine}_${currentAttempt}.mov & echo $! > video_record_${jsengine}_${currentAttempt}.pid`,
);
}

function stopVideoRecording(jsengine, currentAttempt) {
console.log(
`Stop video record using pid: video_record_${jsengine}_${currentAttempt}.pid`,
);
childProcess.exec(
`kill -SIGINT $(cat video_record_${jsengine}_${currentAttempt}.pid)`,
);
}

function executeTestsWithRetries(
appId,
udid,
maestroFlow,
jsengine,
currentAttempt,
) {
try {
startVideoRecording(jsengine, currentAttempt);

const timeout = 1000 * 60 * 10; // 10 minutes
const command = `$HOME/.maestro/bin/maestro --udid="${udid}" test "${maestroFlow}" --format junit -e APP_ID="${appId}"`;
console.log(command);
childProcess.execSync(`MAESTRO_DRIVER_STARTUP_TIMEOUT=1500000 ${command}`, {
stdio: 'inherit',
timeout,
});

stopVideoRecording(jsengine, currentAttempt);
} catch (error) {
// Can't put this in the finally block because it will be executed after the
// recursive call of executeTestsWithRetries
stopVideoRecording(jsengine, currentAttempt);

if (currentAttempt < MAX_ATTEMPTS) {
executeTestsWithRetries(
appId,
udid,
maestroFlow,
jsengine,
currentAttempt + 1,
);
} else {
console.error(`Failed to execute flow after ${MAX_ATTEMPTS} attempts.`);
throw error;
}
}
}

async function main() {
console.info('\n==============================');
console.info('Running tests for iOS with the following parameters:');
console.info(`APP_PATH: ${APP_PATH}`);
console.info(`APP_ID: ${APP_ID}`);
console.info(`MAESTRO_FLOW: ${MAESTRO_FLOW}`);
console.info(`JS_ENGINE: ${JS_ENGINE}`);
console.info(`IS_DEBUG: ${IS_DEBUG}`);
console.info(`WORKING_DIRECTORY: ${WORKING_DIRECTORY}`);
console.info('==============================\n');

const simulatorName = 'iPhone 15 Pro';
launchSimulator(simulatorName);
installAppOnSimulator(APP_PATH);
const udid = extractSimulatorUDID();
bringSimulatorInForeground();
await launchAppOnSimulator(APP_ID, udid, IS_DEBUG);
executeTestsWithRetries(APP_ID, udid, MAESTRO_FLOW, JS_ENGINE, 1);
console.log('Test finished');
process.exit(0);
}

main();

0 comments on commit 93117ea

Please sign in to comment.