infra(e2e): rework local E2E script (#34513)

Summary:
This is a long time coming effort to improve the situation around the local e2e script that in the release crew: the current bash-based script is quirky at best, and what you end up generating as a sample project is not really a true sample project. This is where this PR comes in: it migrates the flow from `./scripts/test-manual-e2e.sh` to `yarn test-e2e-local <options>`.

Here's the current shape of the options:

```sh
Options:
  --help          Show help                                            [boolean]
  --version       Show version number                                  [boolean]
  -t, --target      [choices: "RNTester", "RNTestProject"] [default: "RNTester"]
  -p, --platform                    [choices: "iOS", "Android"] [default: "iOS"]
  -h, --hermes                                         [boolean] [default: true]
```

The idea is to change it so that you can just run the script, and it will do that one specific thing "well", without the tester needing to do anything aside from actually testing the app once it's open.

Some of the key changes:
* tries to stick to the patterns of the other established *.js based scripts, in terms of tooling and approach (and even refactor parts that can be shared with other scripts) - like the android artifacts generation
* no need to start the android emulator on the side
* no need to start Metro on the side
* RNTester iOS will open up on the simulator (no Xcode open that then you need to press)

Things that still need work:
* see the #fixme and #todo in comments
* because we rely on exec, the output sent back is not formatted/shaped correctly so it's a bit more noisy/chaotic - but can't handle it right now because the package we use doesn't allow it - see https://github.com/shelljs/shelljs/issues/86

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
-->

[Internal] [Changed] - Migrate bash E2E local testing script to new JS based command

Pull Request resolved: https://github.com/facebook/react-native/pull/34513

Test Plan:
To test the script, you can run it passing the options showed above; this is the current situation:
* RNTester iOS Hermes 
* RNTester Android Hermes 
* RNTester iOS JSC 
* RNTester Android JSC 
* RNTestProject Android Hermes 
* RNTestProject iOS Hermes 
* RNTestProject Android JSC 
* RNTestProject iOS JSC 

Reviewed By: cortinico

Differential Revision: D39814692

Pulled By: cortinico

fbshipit-source-id: d4791798aaad764c6a3757269b7636f847ccf2ca
This commit is contained in:
Lorenzo Sciandra
2022-10-04 04:56:44 -07:00
committed by Facebook GitHub Bot
parent 540ae39487
commit 97f5ef05e6
11 changed files with 489 additions and 67 deletions
+3 -1
View File
@@ -38,7 +38,7 @@
"rn-get-polyfills.js",
"scripts/compose-source-maps.js",
"scripts/find-node-for-xcode.sh",
"scripts/generate-artifacts.js",
"scripts/generate-codegen-artifacts.js",
"scripts/generate-provider-cli.js",
"scripts/generate-specs-cli.js",
"scripts/codegen/codegen-utils.js",
@@ -95,6 +95,8 @@
"test-android-instrumentation": "yarn run docker-build-android && yarn run test-android-run-instrumentation",
"test-android-unit": "yarn run docker-build-android && yarn run test-android-run-unit",
"test-android-e2e": "yarn run docker-build-android && yarn run test-android-run-e2e",
"test-e2e-local": "node ./scripts/test-e2e-local.js",
"test-e2e-local-clean": "node ./scripts/test-e2e-local-clean.js",
"test-ios": "./scripts/objc-test.sh test",
"test-typescript": "dtslint types"
},
@@ -361,7 +361,7 @@ class CodegenUtilsTests < Test::Unit::TestCase
assert_equal(Pod::Executable.executed_commands, [
{
"command" => "node",
"arguments"=> ["~/app/ios/../node_modules/react-native/scripts/generate-artifacts.js",
"arguments"=> ["~/app/ios/../node_modules/react-native/scripts/generate-codegen-artifacts.js",
"-p", "~/app",
"-o", Pod::Config.instance.installation_root,
"-e", "false",
+3 -3
View File
@@ -201,8 +201,8 @@ class CodegenUtils
end
# Generate input files for in-app libaraies which will be used to check if the script needs to be run.
# TODO: Ideally, we generate the input_files list from generate-artifacts.js and read the result here.
# Or, generate this podspec in generate-artifacts.js as well.
# TODO: Ideally, we generate the input_files list from generate-codegen-artifacts.js and read the result here.
# Or, generate this podspec in generate-codegen-artifacts.js as well.
app_package_path = File.join(app_path, 'package.json')
app_codegen_config = codegen_utils.get_codegen_config_from_file(app_package_path, config_key)
input_files = codegen_utils.get_list_of_js_specs(app_codegen_config, app_path)
@@ -270,7 +270,7 @@ class CodegenUtils
out = Pod::Executable.execute_command(
'node',
[
"#{relative_installation_root}/#{react_native_path}/scripts/generate-artifacts.js",
"#{relative_installation_root}/#{react_native_path}/scripts/generate-codegen-artifacts.js",
"-p", "#{app_path}",
"-o", Pod::Config.instance.installation_root,
"-e", "#{fabric_enabled}",
+7 -60
View File
@@ -31,14 +31,17 @@
* * or otherwise `{major}.{minor}-stable`
*/
const {exec, echo, exit, env, test} = require('shelljs');
const {exec, echo, exit} = require('shelljs');
const {parseVersion} = require('./version-utils');
const {
exitIfNotOnGit,
getCurrentCommit,
isTaggedLatest,
saveFiles,
} = require('./scm-utils');
const {
generateAndroidArtifacts,
saveFilesToRestore,
} = require('./release-utils');
const fs = require('fs');
const os = require('os');
const path = require('path');
@@ -72,22 +75,7 @@ const dryRunBuild = argv.dryRun;
const includeHermes = argv.includeHermes;
const isCommitly = nightlyBuild || dryRunBuild;
const filesToSaveAndRestore = [
'template/Gemfile',
'template/_ruby-version',
'template/package.json',
'.ruby-version',
'Gemfile.lock',
'Gemfile',
'package.json',
'ReactAndroid/gradle.properties',
'Libraries/Core/ReactNativeVersion.js',
'React/Base/RCTVersion.m',
'ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java',
'ReactCommon/cxxreact/ReactNativeVersion.h',
];
saveFiles(filesToSaveAndRestore, tmpPublishingFolder);
saveFilesToRestore(tmpPublishingFolder);
if (includeHermes) {
const HERMES_INSTALL_LOCATION = 'sdks';
@@ -195,48 +183,7 @@ if (isCommitly) {
}
}
// -------- Generating Android Artifacts
env.REACT_NATIVE_SKIP_PREFAB = true;
if (exec('./gradlew :ReactAndroid:installArchives').code) {
echo('Could not generate artifacts');
exit(1);
}
// -------- Generating the Hermes Engine Artifacts
env.REACT_NATIVE_HERMES_SKIP_PREFAB = true;
if (exec('./gradlew :ReactAndroid:hermes-engine:installArchives').code) {
echo('Could not generate artifacts');
exit(1);
}
echo('Generated artifacts for Maven');
let artifacts = [
'.module',
'.pom',
'-debug.aar',
'-release.aar',
'-debug-sources.jar',
'-release-sources.jar',
].map(suffix => {
return `react-native-${releaseVersion}${suffix}`;
});
artifacts.forEach(name => {
if (
!test(
'-e',
`./android/com/facebook/react/react-native/${releaseVersion}/${name}`,
)
) {
echo(
`Failing as expected file: \n\
android/com/facebook/react/react-native/${releaseVersion}/${name}\n\
was not correctly generated.`,
);
exit(1);
}
});
generateAndroidArtifacts(releaseVersion, tmpPublishingFolder);
if (dryRunBuild) {
echo('Skipping `npm publish` because --dry-run is set.');
@@ -100,7 +100,7 @@ generateCodegenArtifactsFromSchema () {
generateArtifacts () {
describe "Generating codegen artifacts"
pushd "$RCT_SCRIPT_RN_DIR" >/dev/null || exit 1
"$NODE_BINARY" "scripts/generate-artifacts.js" --path "$RCT_SCRIPT_APP_PATH" --outputPath "$TEMP_OUTPUT_DIR" --fabricEnabled "$RCT_SCRIPT_FABRIC_ENABLED" --configFileDir "$RCT_SCRIPT_CONFIG_FILE_DIR" --nodeBinary "$NODE_BINARY"
"$NODE_BINARY" "scripts/generate-codegen-artifacts.js" --path "$RCT_SCRIPT_APP_PATH" --outputPath "$TEMP_OUTPUT_DIR" --fabricEnabled "$RCT_SCRIPT_FABRIC_ENABLED" --configFileDir "$RCT_SCRIPT_CONFIG_FILE_DIR" --nodeBinary "$NODE_BINARY"
popd >/dev/null || exit 1
}
+85
View File
@@ -0,0 +1,85 @@
/**
* 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
*/
'use strict';
const {exec, echo, exit, test, env} = require('shelljs');
const {revertFiles, saveFiles} = require('./scm-utils');
function saveFilesToRestore(tmpPublishingFolder) {
const filesToSaveAndRestore = [
'template/Gemfile',
'template/_ruby-version',
'template/package.json',
'.ruby-version',
'Gemfile.lock',
'Gemfile',
'package.json',
'ReactAndroid/gradle.properties',
'Libraries/Core/ReactNativeVersion.js',
'React/Base/RCTVersion.m',
'ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java',
'ReactCommon/cxxreact/ReactNativeVersion.h',
];
saveFiles(filesToSaveAndRestore, tmpPublishingFolder);
}
function generateAndroidArtifacts(releaseVersion, tmpPublishingFolder) {
// -------- Generating Android Artifacts
env.REACT_NATIVE_SKIP_PREFAB = true;
if (exec('./gradlew :ReactAndroid:installArchives').code) {
echo('Could not generate artifacts');
exit(1);
}
// -------- Generating the Hermes Engine Artifacts
env.REACT_NATIVE_HERMES_SKIP_PREFAB = true;
if (exec('./gradlew :ReactAndroid:hermes-engine:installArchives').code) {
echo('Could not generate artifacts');
exit(1);
}
// undo uncommenting javadoc setting
revertFiles(['ReactAndroid/gradle.properties'], tmpPublishingFolder);
echo('Generated artifacts for Maven');
let artifacts = [
'.module',
'.pom',
'-debug.aar',
'-release.aar',
'-debug-sources.jar',
'-release-sources.jar',
].map(suffix => {
return `react-native-${releaseVersion}${suffix}`;
});
artifacts.forEach(name => {
if (
!test(
'-e',
`./android/com/facebook/react/react-native/${releaseVersion}/${name}`,
)
) {
echo(
`Failing as expected file: \n\
android/com/facebook/react/react-native/${releaseVersion}/${name}\n\
was not correctly generated.`,
);
exit(1);
}
});
}
module.exports = {
generateAndroidArtifacts,
saveFilesToRestore,
};
+72
View File
@@ -0,0 +1,72 @@
/**
* 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
*/
'use strict';
/*
* This script, paired with test-e2e-local.js, is the full suite of
* tooling needed for a successful local testing experience.
* This script is an helper to clean up the environment fully
* before running the test suite.
*
* You should use this when switching between branches.
*
* It will:
* - clean up node modules
* - clean up the build folder (derived data, gradlew cleanAll)
* - clean up the pods folder for RNTester (pod install) (and Podfile.lock too)
* - kill all packagers
* - remove RNTestProject folder
*
* an improvements to consider:
* - an option to uninstall the apps (RNTester, RNTestProject) from emulators
*/
const {exec, exit} = require('shelljs');
const {isPackagerRunning} = require('./testing-utils');
console.info('\n** Starting the clean up process **\n');
// let's check if Metro is already running, if it is let's kill it and start fresh
if (isPackagerRunning() === 'running') {
exec("lsof -i :8081 | grep LISTEN | /usr/bin/awk '{print $2}' | xargs kill");
console.info('\n** Killed Metro **\n');
}
// Android
console.info('\n** Cleaning Gradle build artifacts **\n');
exec('./gradlew cleanAll');
// iOS
console.info('\n** Nuking the derived data folder **\n');
exec('rm -rf ~/Library/Developer/Xcode/DerivedData');
// RNTester Pods
console.info('\n** Removing the RNTester Pods **\n');
exec('rm -rf packages/rn-tester/Pods');
// I'm not sure we want to also remove the lock file
// exec('rm -rf packages/rn-tester/Podfile.lock');
// RNTestProject
console.info('\n** Removing the RNTestProject folder **\n');
exec('rm -rf /tmp/RNTestProject');
// final clean up
console.info('\n** Final git level wipe **\n');
// clean unstaged changes from git
exec('git checkout -- .');
// remove all the untracked files
exec('git clean -fdx');
console.info(
'\n** Clean up process completed\nPlease remember to run yarn install if you are planning to test again\n',
);
exit(0);
+205
View File
@@ -0,0 +1,205 @@
/**
* 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
*/
'use strict';
/*
* This script is a re-interpretation of the old test-manual.e2e.sh script.
* the idea is to provide a better DX for the manual testing.
* It's using Javascript over Bash for consistency with the rest of the recent scripts
* and to make it more accessible for other devs to play around with.
*/
const {exec, exit, pushd, popd, pwd, cd} = require('shelljs');
const yargs = require('yargs');
const fs = require('fs');
const path = require('path');
const os = require('os');
const {
launchAndroidEmulator,
isPackagerRunning,
launchPackagerInSeparateWindow,
} = require('./testing-utils');
const {
generateAndroidArtifacts,
saveFilesToRestore,
} = require('./release-utils');
const argv = yargs
.option('t', {
alias: 'target',
default: 'RNTester',
choices: ['RNTester', 'RNTestProject'],
})
.option('p', {
alias: 'platform',
default: 'iOS',
choices: ['iOS', 'Android'],
})
.option('h', {
alias: 'hermes',
type: 'boolean',
default: true,
}).argv;
/*
* see the test-local-e2e.js script for clean up process
*/
// command order: we ask the user to select if they want to test RN tester
// or RNTestProject
// if they select RN tester, we ask if iOS or Android, and then we run the tests
// if they select RNTestProject, we run the RNTestProject test
// let's check if Metro is already running, if it is let's kill it and start fresh
if (isPackagerRunning() === 'running') {
exec("lsof -i :8081 | grep LISTEN | /usr/bin/awk '{print $2}' | xargs kill");
}
if (argv.target === 'RNTester') {
// FIXME: make sure that the commands retains colors
// (--ansi) doesn't always work
// see also https://github.com/shelljs/shelljs/issues/86
if (argv.platform === 'iOS') {
console.info(
`We're going to test the ${
argv.hermes ? 'Hermes' : 'JSC'
} version of RNTester iOS`,
);
exec(
`cd packages/rn-tester && USE_HERMES=${
argv.hermes ? 1 : 0
} bundle exec pod install --ansi`,
);
// if everything succeeded so far, we can launch Metro and the app
// start the Metro server in a separate window
launchPackagerInSeparateWindow();
// launch the app on iOS simulator
pushd('packages/rn-tester');
exec('npx react-native run-ios --scheme RNTester');
popd();
} else {
// we do the android path here
launchAndroidEmulator();
console.info(
`We're going to test the ${
argv.hermes ? 'Hermes' : 'JSC'
} version of RNTester Android`,
);
exec(
`./gradlew :packages:rn-tester:android:app:${
argv.hermes ? 'installHermesDebug' : 'installJscDebug'
} --quiet`,
);
// launch the app on Android simulator
// TODO: we should find a way to make it work like for iOS, via npx react-native run-android
// currently, that fails with an error.
// if everything succeeded so far, we can launch Metro and the app
// start the Metro server in a separate window
launchPackagerInSeparateWindow();
// just to make sure that the Android up won't have troubles finding the Metro server
exec('adb reverse tcp:8081 tcp:8081');
// launch the app
exec(
'adb shell am start -n com.facebook.react.uiapp/com.facebook.react.uiapp.RNTesterActivity',
);
}
} else {
console.info("We're going to test a fresh new RN project");
// create the local npm package to feed the CLI
// base setup required (specular to publish-npm.js)
const tmpPublishingFolder = fs.mkdtempSync(
path.join(os.tmpdir(), 'rn-publish-'),
);
console.info(`The temp publishing folder is ${tmpPublishingFolder}`);
saveFilesToRestore(tmpPublishingFolder);
// we need to add the unique timestamp to avoid npm/yarn to use some local caches
const baseVersion = require('../package.json').version;
const dateIdentifier = new Date()
.toISOString()
.slice(0, -8)
.replace(/[-:]/g, '')
.replace(/[T]/g, '-');
const releaseVersion = `${baseVersion}-${dateIdentifier}`;
// this is needed to generate the Android artifacts correctly
exec(`node scripts/set-rn-version.js --to-version ${releaseVersion}`).code;
// Generate native files (Android only for now)
generateAndroidArtifacts(releaseVersion, tmpPublishingFolder);
// create locally the node module
exec('npm pack');
const localNodeTGZPath = `${pwd()}/react-native-${releaseVersion}.tgz`;
exec(`node scripts/set-rn-template-version.js "file:${localNodeTGZPath}"`);
const repoRoot = pwd();
pushd('/tmp/');
// need to avoid the pod install step because it will fail! (see above)
exec(
`node ${repoRoot}/cli.js init RNTestProject --template ${repoRoot} --skip-install`,
);
cd('RNTestProject');
exec('yarn install');
if (argv.platform === 'iOS') {
// if we want iOS, we need to do pod install - but with a trick
cd('ios');
exec('bundle install');
// TODO: we should be able to also use HERMES_ENGINE_TARBALL_PATH
// if we can make RNTester step generate it already so that it gets reused
// need to discern if it's main branch or release branch
if (baseVersion === '1000.0.0') {
// main branch
exec(`USE_HERMES=${argv.hermes ? 1 : 0} bundle exec pod install --ansi`);
} else {
// TODO: to test this, I need to apply changes on top of a release branch
// a release branch
// copy over the .hermesversion file from react-native core into the RNTestProject
exec(`cp -f ${repoRoot}/sdks/.hermesversion .`);
exec(
`CI=true USE_HERMES=${
argv.hermes ? 1 : 0
} bundle exec pod install --ansi`,
);
}
cd('..');
exec('yarn ios');
} else {
// android
exec('yarn android');
}
popd();
// just cleaning up the temp folder, the rest is done by the test clean script
exec(`rm -rf ${tmpPublishingFolder}`);
}
exit(0);
+111
View File
@@ -0,0 +1,111 @@
/**
* 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
*/
'use strict';
const {exec} = require('shelljs');
const os = require('os');
const {spawn} = require('node:child_process');
/*
* Android related utils - leverages android tooling
*/
// this code is taken from the CLI repo, slightly readapted to our needs
// here's the reference folder:
// https://github.com/react-native-community/cli/blob/main/packages/cli-platform-android/src/commands/runAndroid
const emulatorCommand = process.env.ANDROID_HOME
? `${process.env.ANDROID_HOME}/emulator/emulator`
: 'emulator';
const getEmulators = () => {
const emulatorsOutput = exec(`${emulatorCommand} -list-avds`).stdout;
return emulatorsOutput.split(os.EOL).filter(name => name !== '');
};
const launchEmulator = emulatorName => {
// we need both options 'cause reasons:
// from docs: "When using the detached option to start a long-running process, the process will not stay running in the background after the parent exits unless it is provided with a stdio configuration that is not connected to the parent. If the parent's stdio is inherited, the child will remain attached to the controlling terminal."
// here: https://nodejs.org/api/child_process.html#optionsdetached
const cp = spawn(emulatorCommand, [`@${emulatorName}`], {
detached: true,
stdio: 'ignore',
});
cp.unref();
};
function tryLaunchEmulator() {
const emulators = getEmulators();
if (emulators.length > 0) {
try {
launchEmulator(emulators[0]);
return {success: true};
} catch (error) {
return {success: false, error};
}
}
return {
success: false,
error: 'No emulators found as an output of `emulator -list-avds`',
};
}
function launchAndroidEmulator() {
const result = tryLaunchEmulator();
if (result.success) {
console.info('Successfully launched emulator.');
} else {
console.error(`Failed to launch emulator. Reason: ${result.error || ''}.`);
console.warn(
'Please launch an emulator manually or connect a device. Otherwise app may fail to launch.',
);
}
}
/*
* iOS related utils - leverages xcodebuild
*/
/*
* Metro related utils
*/
// inspired by CLI again https://github.com/react-native-community/cli/blob/main/packages/cli-tools/src/isPackagerRunning.ts
function isPackagerRunning(
packagerPort = process.env.RCT_METRO_PORT || '8081',
) {
try {
const status = exec(`curl http://localhost:${packagerPort}/status`, {
silent: true,
}).stdout;
return status === 'packager-status:running' ? 'running' : 'unrecognized';
} catch (_error) {
return 'not_running';
}
}
// this is a very limited implementation of how this should work
// literally, this is macos only
// a more robust implementation can be found here:
// https://github.com/react-native-community/cli/blob/7c003f2b1d9d80ec5c167614ba533a004272c685/packages/cli-platform-android/src/commands/runAndroid/index.ts#L195
function launchPackagerInSeparateWindow() {
exec("open -a 'Terminal' ./scripts/packager.sh");
}
module.exports = {
launchAndroidEmulator,
isPackagerRunning,
launchPackagerInSeparateWindow,
};
+1 -1
View File
@@ -28,7 +28,7 @@ git = "https://github.com/facebook/hermes.git"
if ENV.has_key?('HERMES_ENGINE_TARBALL_PATH')
Pod::UI.puts '[Hermes] Using pre-built Hermes binaries from local path.' if Object.const_defined?("Pod::UI")
source[:http] = "file://#{ENV['HERMES_ENGINE_TARBALL_PATH']}"
elsif version == '1000.0.0' || version.start_with?('0.0.0-')
elsif version.include? '1000.0.0'
Pod::UI.puts '[Hermes] Installing hermes-engine may take a while, building Hermes from source...'.yellow if Object.const_defined?("Pod::UI")
source[:git] = git
source[:commit] = `git ls-remote https://github.com/facebook/hermes main | cut -f 1`.strip