diff --git a/ContainerShip/Dockerfile.android b/ContainerShip/Dockerfile.android new file mode 100644 index 000000000..9feadf159 --- /dev/null +++ b/ContainerShip/Dockerfile.android @@ -0,0 +1,55 @@ +FROM containership/android-base:latest + +# set default environment variables +ENV GRADLE_OPTS="-Dorg.gradle.jvmargs=\"-Xmx512m -XX:+HeapDumpOnOutOfMemoryError\"" +ENV JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF8" +ENV REACT_NATIVE_MAX_WORKERS=1 + +# add ReactAndroid directory +ADD .buckconfig /app/.buckconfig +ADD .buckjavaargs /app/.buckjavaargs +ADD ReactAndroid /app/ReactAndroid +ADD ReactCommon /app/ReactCommon +ADD keystores /app/keystores + +# set workdir +WORKDIR /app + +# run buck fetches +RUN buck fetch ReactAndroid/src/test/java/com/facebook/react/modules +RUN buck fetch ReactAndroid/src/main/java/com/facebook/react +RUN buck fetch ReactAndroid/src/main/java/com/facebook/react/shell +RUN buck fetch ReactAndroid/src/test/... +RUN buck fetch ReactAndroid/src/androidTest/... + +# build app +RUN buck build ReactAndroid/src/main/java/com/facebook/react +RUN buck build ReactAndroid/src/main/java/com/facebook/react/shell + +ADD gradle /app/gradle +ADD gradlew /app/gradlew +ADD settings.gradle /app/settings.gradle +ADD build.gradle /app/build.gradle +ADD react.gradle /app/react.gradle + +# run gradle downloads +RUN ./gradlew :ReactAndroid:downloadBoost :ReactAndroid:downloadDoubleConversion :ReactAndroid:downloadFolly :ReactAndroid:downloadGlog + +# compile native libs with Gradle script, we need bridge for unit and integration tests +RUN ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=1 -Pcom.android.build.threadPoolSize=1 + +# add all react-native code +ADD . /app +WORKDIR /app + +# https://github.com/npm/npm/issues/13306 +RUN cd $(npm root -g)/npm && npm install fs-extra && sed -i -e s/graceful-fs/fs-extra/ -e s/fs.rename/fs.move/ ./lib/utils/rename.js + +# build node dependencies +RUN npm install +RUN npm install github@0.2.4 + +WORKDIR /app/website +RUN npm install + +WORKDIR /app diff --git a/ContainerShip/Dockerfile.android-base b/ContainerShip/Dockerfile.android-base new file mode 100644 index 000000000..47475078b --- /dev/null +++ b/ContainerShip/Dockerfile.android-base @@ -0,0 +1,78 @@ +FROM library/ubuntu:16.04 + +# set default build arguments +ARG ANDROID_VERSION=25.2.3 +ARG BUCK_VERSION=2016.11.11.01 +ARG NDK_VERSION=10e +ARG NODE_VERSION=6.2.0 +ARG WATCHMAN_VERSION=4.7.0 + +# set default environment variables +ENV ADB_INSTALL_TIMEOUT=10 +ENV PATH=${PATH}:/opt/buck/bin/ +ENV ANDROID_HOME=/opt/android +ENV ANDROID_SDK_HOME=${ANDROID_HOME} +ENV PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools +ENV ANDROID_NDK=/opt/ndk/android-ndk-r$NDK_VERSION +ENV PATH=${PATH}:${ANDROID_NDK} + +# install system dependencies +RUN apt-get update && apt-get install ant autoconf automake curl g++ gcc git libqt5widgets5 lib32z1 lib32stdc++6 make maven npm openjdk-8* python-dev python3-dev qml-module-qtquick-controls qtdeclarative5-dev unzip -y + +# configure npm +RUN npm config set spin=false +RUN npm config set progress=false + +# install node +RUN npm install n -g +RUN n $NODE_VERSION + +# download buck +RUN git clone https://github.com/facebook/buck.git /opt/buck +WORKDIR /opt/buck +RUN git checkout v$BUCK_VERSION + +# build buck +RUN ant + +# download watchman +RUN git clone https://github.com/facebook/watchman.git /opt/watchman +WORKDIR /opt/watchman +RUN git checkout v$WATCHMAN_VERSION + +# build watchman +RUN ./autogen.sh +RUN ./configure +RUN make +RUN make install + +# download and unpack android +RUN mkdir /opt/android +WORKDIR /opt/android +RUN curl --silent https://dl.google.com/android/repository/tools_r$ANDROID_VERSION-linux.zip > android.zip +RUN unzip android.zip +RUN rm android.zip + +# download and unpack NDK +RUN mkdir /opt/ndk +WORKDIR /opt/ndk +RUN curl --silent https://dl.google.com/android/repository/android-ndk-r$NDK_VERSION-linux-x86_64.zip > ndk.zip +RUN unzip ndk.zip + +# cleanup NDK +RUN rm ndk.zip + +# add android SDK tools +# 2 - Android SDK Platform-tools, revision 25.0.3 +# 12 - Android SDK Build-tools, revision 23.0.1 +# 35 - SDK Platform Android 6.0, API 23, revision 3 +# 39 - SDK Platform Android 4.4.2, API 19, revision 4 +# 102 - ARM EABI v7a System Image, Android API 19, revision 5 +# 103 - Intel x86 Atom System Image, Android API 19, revision 5 +# 131 - Google APIs, Android API 23, revision 1 +# 166 - Android Support Repository, revision 41 +RUN echo "y" | android update sdk -u -a -t 2,12,35,39,102,103,131,166 +RUN ln -s /opt/android/platform-tools/adb /usr/bin/adb + +# clean up unnecessary directories +RUN rm -rf /opt/android/system-images/android-19/default/x86 diff --git a/ContainerShip/Dockerfile.javascript b/ContainerShip/Dockerfile.javascript new file mode 100644 index 000000000..d9fc1f57c --- /dev/null +++ b/ContainerShip/Dockerfile.javascript @@ -0,0 +1,19 @@ +FROM library/node:6.9.2 + +ENV YARN_VERSION=0.19.1 + +# install dependencies +RUN apt-get update && apt-get install ocaml libelf-dev -y +RUN npm install yarn@$YARN_VERSION -g + +# add code +RUN mkdir /app +ADD . /app + +WORKDIR /app +RUN yarn install --ignore-engines + +WORKDIR website +RUN yarn install --ignore-engines --ignore-platform + +WORKDIR /app diff --git a/ContainerShip/scripts/run-android-ci-instrumentation-tests.js b/ContainerShip/scripts/run-android-ci-instrumentation-tests.js new file mode 100644 index 000000000..fe29cbac1 --- /dev/null +++ b/ContainerShip/scripts/run-android-ci-instrumentation-tests.js @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +/** + * This script runs instrumentation tests one by one with retries + * Instrumentation tests tend to be flaky, so rerunning them individually increases + * chances for success and reduces total average execution time. + * + * We assume that all instrumentation tests are flat in one folder + * Available arguments: + * --path - path to all .java files with tests + * --package - com.facebook.react.tests + * --retries [num] - how many times to retry possible flaky commands: npm install and running tests, default 1 + */ +/*eslint-disable no-undef */ + +const argv = require('yargs').argv; +const async = require('async'); +const child_process = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const colors = { + GREEN: '\x1b[32m', + RED: '\x1b[31m', + RESET: '\x1b[0m' +}; + +const test_opts = { + FILTER: new RegExp(argv.filter || '.*', 'i'), + PACKAGE: argv.package || 'com.facebook.react.tests', + PATH: argv.path || './ReactAndroid/src/androidTest/java/com/facebook/react/tests', + RETRIES: parseInt(argv.retries || 2, 10), + + TEST_TIMEOUT: parseInt(argv['test-timeout'] || 1000 * 60 * 10), + + OFFSET: argv.offset, + COUNT: argv.count +} + +let max_test_class_length = Number.NEGATIVE_INFINITY; + +let testClasses = fs.readdirSync(path.resolve(process.cwd(), test_opts.PATH)) + .filter((file) => { + return file.endsWith('.java'); + }).map((clazz) => { + return path.basename(clazz, '.java'); + }).map((clazz) => { + return test_opts.PACKAGE + '.' + clazz; + }).filter((clazz) => { + return test_opts.FILTER.test(clazz); + }); + +// only process subset of the tests at corresponding offset and count if args provided +if (test_opts.COUNT != null && test_opts.OFFSET != null) { + const testCount = testClasses.length; + const start = test_opts.COUNT * test_opts.OFFSET; + const end = start + test_opts.COUNT; + + if (start >= testClasses.length) { + testClasses = []; + } else if (end >= testClasses.length) { + testClasses = testClasses.slice(start); + } else { + testClasses = testClasses.slice(start, end); + } +} + +return async.mapSeries(testClasses, (clazz, callback) => { + if(clazz.length > max_test_class_length) { + max_test_class_length = clazz.length; + } + + return async.retry(test_opts.RETRIES, (retryCb) => { + const test_process = child_process.spawn('./ContainerShip/scripts/run-instrumentation-tests-via-adb-shell.sh', [test_opts.PACKAGE, clazz], { + stdio: 'inherit' + }) + + const timeout = setTimeout(() => { + test_process.kill(); + }, test_opts.TEST_TIMEOUT); + + test_process.on('error', (err) => { + clearTimeout(timeout); + retryCb(err); + }); + + test_process.on('exit', (code) => { + clearTimeout(timeout); + + if(code !== 0) { + return retryCb(new Error(`Process exited with code: ${code}`)); + } + + return retryCb(); + }); + }, (err) => { + return callback(null, { + name: clazz, + status: err ? 'failure' : 'success' + }); + }); +}, (err, results) => { + print_test_suite_results(results); + + const failures = results.filter((test) => { + test.status === 'failure'; + }); + + return failures.length === 0 ? process.exit(0) : process.exit(1); +}); + +function print_test_suite_results(results) { + console.log('\n\nTest Suite Results:\n'); + + let color; + let failing_suites = 0; + let passing_suites = 0; + + function pad_output(num_chars) { + let i = 0; + + while(i < num_chars) { + process.stdout.write(' '); + i++; + } + } + results.forEach((test) => { + if(test.status === 'success') { + color = colors.GREEN; + passing_suites++; + } else if(test.status === 'failure') { + color = colors.RED; + failing_suites++; + } + + process.stdout.write(color); + process.stdout.write(test.name); + pad_output((max_test_class_length - test.name.length) + 8); + process.stdout.write(test.status); + process.stdout.write(`${colors.RESET}\n`); + }); + + console.log(`\n${passing_suites} passing, ${failing_suites} failing!`); +} diff --git a/ContainerShip/scripts/run-android-docker-instrumentation-tests.sh b/ContainerShip/scripts/run-android-docker-instrumentation-tests.sh new file mode 100644 index 000000000..9d3f768f4 --- /dev/null +++ b/ContainerShip/scripts/run-android-docker-instrumentation-tests.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# for buck gen +mount -o remount,exec /dev/shm + +AVD_UUID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1) + +# create virtual device +echo no | android create avd -n $AVD_UUID -f -t android-19 --abi default/armeabi-v7a + +# emulator setup +emulator64-arm -avd $AVD_UUID -no-skin -no-audio -no-window -no-boot-anim & +bootanim="" +until [[ "$bootanim" =~ "stopped" ]]; do + sleep 5 + bootanim=$(adb -e shell getprop init.svc.bootanim 2>&1) + echo "boot animation status=$bootanim" +done + +set -x + +# solve issue with max user watches limit +echo 65536 | tee -a /proc/sys/fs/inotify/max_user_watches +watchman shutdown-server + +# integration tests +# build JS bundle for instrumentation tests +node local-cli/cli.js bundle --platform android --dev true --entry-file ReactAndroid/src/androidTest/js/TestBundle.js --bundle-output ReactAndroid/src/androidTest/assets/AndroidTestBundle.js + +# build test APK +buck install ReactAndroid/src/androidTest/buck-runner:instrumentation-tests --config build.threads=1 + +# run installed apk with tests +node ./ContainerShip/scripts/run-android-ci-instrumentation-tests.js $* diff --git a/ContainerShip/scripts/run-android-docker-unit-tests.sh b/ContainerShip/scripts/run-android-docker-unit-tests.sh new file mode 100644 index 000000000..5a58bb352 --- /dev/null +++ b/ContainerShip/scripts/run-android-docker-unit-tests.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# set default environment variables +UNIT_TESTS_BUILD_THREADS="${UNIT_TESTS_BUILD_THREADS:-1}" + +# for buck gen +mount -o remount,exec /dev/shm + +set -x + +# run unit tests +buck test ReactAndroid/src/test/... --config build.threads=$UNIT_TESTS_BUILD_THREADS diff --git a/ContainerShip/scripts/run-ci-e2e-tests.sh b/ContainerShip/scripts/run-ci-e2e-tests.sh new file mode 100755 index 000000000..e6739dfe9 --- /dev/null +++ b/ContainerShip/scripts/run-ci-e2e-tests.sh @@ -0,0 +1,250 @@ +#!/bin/bash + +set -ex + +# set default environment variables +ROOT=$(pwd) +SCRIPTS=$(pwd)/scripts + +RUN_ANDROID=0 +RUN_CLI_INSTALL=1 +RUN_IOS=0 +RUN_JS=0 + +RETRY_COUNT=${RETRY_COUNT:-3} +AVD_UUID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1) + +ANDROID_NPM_DEPS="appium@1.5.1 mocha@2.4.5 wd@0.3.11 colors@1.0.3 pretty-data2@0.40.1" +CLI_PACKAGE=$ROOT/react-native-cli/react-native-cli-*.tgz +PACKAGE=$ROOT/react-native-*.tgz +REACT_NATIVE_MAX_WORKERS=1 + +# retries command on failure +# $1 -- max attempts +# $2 -- command to run +function retry() { + local -r -i max_attempts="$1"; shift + local -r cmd="$@" + local -i attempt_num=1 + + until $cmd; do + if (( attempt_num == max_attempts )); then + echo "Execution of '$cmd' failed; no more attempts left" + return 1 + else + (( attempt_num++ )) + echo "Execution of '$cmd' failed; retrying for attempt number $attempt_num..." + fi + done +} + +# parse command line args & flags +while :; do + case "$1" in + --android) + RUN_ANDROID=1 + shift + ;; + + --ios) + RUN_IOS=1 + shift + ;; + + --js) + RUN_JS=1 + shift + ;; + + --skip-cli-install) + RUN_CLI_INSTALL=0 + shift + ;; + + --tvos) + RUN_IOS=1 + shift + ;; + + *) + break + esac +done + +function e2e_suite() { + cd $ROOT + + if [ $RUN_ANDROID -eq 0 ] && [ $RUN_IOS -eq 0 ] && [ $RUN_JS -eq 0 ]; then + echo "No e2e tests specified!" + return 0 + fi + + # create temp dir + TEMP_DIR=$(mktemp -d /tmp/react-native-XXXXXXXX) + + # To make sure we actually installed the local version + # of react-native, we will create a temp file inside the template + # and check that it exists after `react-native init + IOS_MARKER=$(mktemp $ROOT/local-cli/templates/HelloWorld/ios/HelloWorld/XXXXXXXX) + ANDROID_MARKER=$(mktemp ${ROOT}/local-cli/templates/HelloWorld/android/XXXXXXXX) + + # install CLI + cd react-native-cli + npm pack + cd .. + + # can skip cli install for non sudo mode + if [ $RUN_CLI_INSTALL -ne 0 ]; then + npm install -g $CLI_PACKAGE + if [ $? -ne 0 ]; then + echo "Could not install react-native-cli globally, please run in su mode" + echo "Or with --skip-cli-install to skip this step" + return 1 + fi + fi + + if [ $RUN_ANDROID -ne 0 ]; then + set +ex + + # create virtual device + if ! android list avd | grep "$AVD_UUID" > /dev/null; then + echo no | android create avd -n $AVD_UUID -f -t android-19 --abi default/armeabi-v7a + fi + + # newline at end of adb devices call and first line is headers + DEVICE_COUNT=$(adb devices | wc -l) + ((DEVICE_COUNT -= 2)) + + # will always kill an existing emulator if one exists for fresh setup + if [[ $DEVICE_COUNT -ge 1 ]]; then + adb emu kill + fi + + # emulator setup + emulator64-arm -avd $AVD_UUID -no-skin -no-audio -no-window -no-boot-anim & + + bootanim="" + until [[ "$bootanim" =~ "stopped" ]]; do + sleep 5 + bootanim=$(adb -e shell getprop init.svc.bootanim 2>&1) + echo "boot animation status=$bootanim" + done + + set -ex + + ./gradlew :ReactAndroid:installArchives -Pjobs=1 -Dorg.gradle.jvmargs="-Xmx512m -XX:+HeapDumpOnOutOfMemoryError" + if [ $? -ne 0 ]; then + echo "Failed to compile Android binaries" + return 1 + fi + fi + + npm pack + if [ $? -ne 0 ]; then + echo "Failed to pack react-native" + return 1 + fi + + cd $TEMP_DIR + + retry $RETRY_COUNT react-native init EndToEndTest --version $PACKAGE --npm + if [ $? -ne 0 ]; then + echo "Failed to execute react-native init" + echo "Most common reason is npm registry connectivity, try again" + return 1 + fi + + cd EndToEndTest + + # android tests + if [ $RUN_ANDROID -ne 0 ]; then + echo "Running an Android e2e test" + echo "Installing e2e framework" + + retry $RETRY_COUNT npm install --save-dev $ANDROID_NPM_DEPS --silent >> /dev/null + if [ $? -ne 0 ]; then + echo "Failed to install appium" + echo "Most common reason is npm registry connectivity, try again" + return 1 + fi + + cp $SCRIPTS/android-e2e-test.js android-e2e-test.js + + cd android + echo "Downloading Maven deps" + ./gradlew :app:copyDownloadableDepsToLibs + + cd .. + keytool -genkey -v -keystore android/keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US" + + echo "Starting packager server" + node ./node_modules/.bin/appium >> /dev/null & + APPIUM_PID=$! + echo "Starting appium server $APPIUM_PID" + + echo "Building app" + buck build android/app + + # hack to get node unhung (kill buckd) + kill -9 $(pgrep java) + + if [ $? -ne 0 ]; then + echo "could not execute Buck build, is it installed and in PATH?" + return 1 + fi + + npm start >> /dev/null & + SERVER_PID=$! + sleep 15 + + echo "Executing android e2e test" + retry $RETRY_COUNT node node_modules/.bin/_mocha android-e2e-test.js + if [ $? -ne 0 ]; then + echo "Failed to run Android e2e tests" + echo "Most likely the code is broken" + return 1 + fi + + # kill packager process + if kill -0 $SERVER_PID; then + echo "Killing packager $SERVER_PID" + kill -9 $SERVER_PID + fi + + # kill appium process + if kill -0 $APPIUM_PID; then + echo "Killing appium $APPIUM_PID" + kill -9 $APPIUM_PID + fi + + fi + + # ios tests + if [ $RUN_IOS -ne 0 ]; then + echo "Running ios e2e tests not yet implemented for docker!" + fi + + # js tests + if [ $RUN_JS -ne 0 ]; then + # Check the packager produces a bundle (doesn't throw an error) + REACT_NATIVE_MAX_WORKERS=1 react-native bundle --platform android --dev true --entry-file index.android.js --bundle-output android-bundle.js + if [ $? -ne 0 ]; then + echo "Could not build android bundle" + return 1 + fi + + REACT_NATIVE_MAX_WORKERS=1 react-native bundle --platform ios --dev true --entry-file index.ios.js --bundle-output ios-bundle.js + if [ $? -ne 0 ]; then + echo "Could not build iOS bundle" + return 1 + fi + fi + + # directory cleanup + rm $IOS_MARKER + rm $ANDROID_MARKER + + return 0 +} + +retry $RETRY_COUNT e2e_suite diff --git a/ContainerShip/scripts/run-instrumentation-tests-via-adb-shell.sh b/ContainerShip/scripts/run-instrumentation-tests-via-adb-shell.sh new file mode 100755 index 000000000..ea5255052 --- /dev/null +++ b/ContainerShip/scripts/run-instrumentation-tests-via-adb-shell.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Python script to run instrumentation tests, copied from https://github.com/circleci/circle-dummy-android +# Example: ./scripts/run-android-instrumentation-tests.sh com.facebook.react.tests com.facebook.react.tests.ReactPickerTestCase +# +export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$PATH" + +# clear the logs +adb logcat -c + +# run tests and check output +python - $1 $2 << END + +import re +import subprocess as sp +import sys +import threading +import time + +done = False + +test_app = sys.argv[1] +test_class = None + +if len(sys.argv) > 2: + test_class = sys.argv[2] + +def update(): + # prevent CircleCI from killing the process for inactivity + while not done: + time.sleep(5) + print "Running in background. Waiting for 'adb' command reponse..." + +t = threading.Thread(target=update) +t.dameon = True +t.start() + +def run(): + sp.Popen(['adb', 'wait-for-device']).communicate() + if (test_class != None): + p = sp.Popen('adb shell am instrument -w -e class %s %s/android.support.test.runner.AndroidJUnitRunner' + % (test_class, test_app), shell=True, stdout=sp.PIPE, stderr=sp.PIPE, stdin=sp.PIPE) + else : + p = sp.Popen('adb shell am instrument -w %s/android.support.test.runner.AndroidJUnitRunner' + % (test_app), shell=True, stdout=sp.PIPE, stderr=sp.PIPE, stdin=sp.PIPE) + return p.communicate() + +success = re.compile(r'OK \(\d+ test(s)?\)') +stdout, stderr = run() + +done = True +print stderr +print stdout + +if success.search(stderr + stdout): + sys.exit(0) +else: + # dump the logs + sp.Popen(['adb', 'logcat', '-d']).communicate() + sys.exit(1) # make sure we fail if the test failed +END + +RETVAL=$? + +exit $RETVAL diff --git a/DockerTests.md b/DockerTests.md new file mode 100644 index 000000000..790d19527 --- /dev/null +++ b/DockerTests.md @@ -0,0 +1,96 @@ +# Dockerfile Tests + +This is a high level overview of the test configuration using docker. It explains how to run the tests locally +and how they integrate with the Jenkins Pipeline script to run the automated tests on ContainerShip . + +## Docker Installation + +It is required to have Docker running on your machine in order to build and run the tests in the Dockerfiles. +See for more information on how to install. + +## Convenience NPM Run Scripts + +We have added a number of default run scripts to the `package.json` file to simplify building and running your tests. + +`npm run test-android-setup` - Pulls down the base android docker image used for running the tests + +`npm run test-android-build` - Builds the docker image used to run the tests + +`npm run test-android-run-unit` - Runs all the unit tests that have been built in the latest react/android docker image (note: you need to run test-android-build before executing this, if the image does not exist it will fail) + +`npm run test-android-run-instrumentation` - Runs all the instrumentation tests that have been built in the latest react/android docker image (note: you need to run test-android-build before executing this, if the image does not exist it will fail). You can also pass additional flags to filter which tests instrumentation tests are run. Ex: `npm run test-android-run-instrumentation -- --filter=TestIdTestCase` to only run the TestIdTestCase instrumentation test. See below for more information +on the instrumentation test flags. + +`npm run test-android-run-e2e` - Runs all the end to end tests that have been built in the latest react/android docker image (note: you need to run test-android-build before executing this, if the image does not exist it will fail) + +`npm run test-android-unit` - Builds and runs the android unit tests. + +`npm run test-android-instrumentation` - Builds and runs the android instrumentation tests. + +`npm run test-android-e2e` - Builds and runs the android end to end tests. + +## Detailed Android Setup + +There are two Dockerfiles for use with the Android codebase. + +The `Dockerfile.android-base` contains all the necessary prerequisites required to run the React Android tests. It is +separated out into a separate Dockerfile because these are dependencies that rarely change and also because it is quite +a beastly image since it contains all the Android depedencies for running android and the emulators (~9GB). + +The good news is you should rarely have to build or pull down the base image! All iterative code updates happen as +part of the `Dockerfile.android` image build. + +So step one... + +`docker pull containership/android-base:latest` + +This will take quite some time depending on your connection and you need to ensure you have ~10GB of free disk space. + +Once this is done, you can run tests locally by executing two simple commands: + +1. `docker build -t react/android -f ./ContainerShip/Dockerfile.android .` +2. `docker run --cap-add=SYS_ADMIN -it react/android bash ContainerShip/scripts/run-android-docker-unit-tests.sh` + +> Note: `--cap-add=SYS_ADMIN` flag is required for the `ContainerShip/scripts/run-android-docker-unit-tests.sh` and +`ContainerShip/scripts/run-android-docker-instrumentation-tests.sh` in order to allow the remounting of `/dev/shm` as writeable +so the `buck` build system may write temporary output to that location + +Every time you make any modifications to the codebase, you should re-run the `docker build ...` command in order for your +updates to be included in your local docker image. + +The following shell scripts have been provided for android testing: + +`ContainerShip/scripts/run-android-docker-unit-tests.sh` - Runs the standard android unit tests + +`ContainerShip/scripts/run-android-docker-instrumentation-tests.sh` - Runs the android instrumentation tests on the emulator. *Note* that these +tests take quite some time to run so there are various flags you can pass in order to filter which tests are run (see below) + +`ContainerShip/scripts/run-ci-e2e-tests.sh` - Runs the android end to end tests + +#### ContainerShip/scripts/run-android-docker-instrumentation-tests.sh + +The instrumentation test script accepts the following flags in order to customize the execution of the tests: + +`--filter` - A regex that filters which instrumentation tests will be run. (Defaults to .*) + +`--package` - Name of the java package containing the instrumentation tests (Defaults to com.facebook.react.tests) + +`--path` - Path to the directory containing the instrumentation tests. (Defaults to ./ReactAndroid/src/androidTest/java/com/facebook/react/tests) + +`--retries` - Number of times to retry a failed test before declaring a failure (Defaults to 2) + +For example, if locally you only wanted to run the InitialPropsTestCase, you could do the following: + +`docker run --cap-add=SYS_ADMIN -it react/android bash ContainerShip/scripts/run-android-docker-instrumentation-tests.sh --filter="InitialPropsTestCase"` + +# Javascript Setup + +There is a single Dockerfile for use with the javascript codebase. + +The `Dockerfile.javascript` base requires all the necessary dependencies for running Javascript tests. + +Any time you make an update to the codebase, you can build and run the javascript tests with the following three commands: + +1. `docker build -t react/js -f ./ContainerShip/Dockerfile.javascript .` +2. `docker run -it react/js yarn test --maxWorkers=4` +3. `docker run -it react/js yarn run flow -- check` diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..3563e6bbb --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,189 @@ +import groovy.json.JsonSlurperClassic + +def runPipeline() { + try { + ansiColor('xterm') { + runStages(); + } + } catch(err) { + echo "Error: ${err}" + currentBuild.result = "FAILED" + } +} + +def pullDockerImage(imageName) { + def result = sh(script: "docker pull ${imageName}", returnStatus: true) + + if (result != 0) { + throw new Exception("Failed to pull image[${imageName}]") + } +} + +def buildDockerfile(dockerfilePath = "Dockerfile", imageName) { + def buildCmd = "docker build -f ${dockerfilePath} -t ${imageName} ." + echo "${buildCmd}" + + def result = sh(script: buildCmd, returnStatus: true) + + if (result != 0) { + throw new Exception("Failed to build image[${imageName}] from '${dockerfilePath}'") + } +} + +def runCmdOnDockerImage(imageName, cmd, run_opts = '') { + def result = sh(script: "docker run ${run_opts} -i ${imageName} sh -c '${cmd}'", returnStatus: true) + + if(result != 0) { + throw new Exception("Failed to run cmd[${cmd}] on image[${imageName}]") + } +} + +def calculateGithubInfo() { + return [ + branch: env.BRANCH_NAME, + sha: sh(returnStdout: true, script: 'git rev-parse HEAD').trim(), + tag: null, + isPR: "${env.CHANGE_URL}".contains('/pull/') + ] +} + +def getParallelInstrumentationTests(testDir, parallelCount, imageName) { + def integrationTests = [:] + def testCount = sh(script: "ls ${testDir} | wc -l", returnStdout: true).trim().toInteger() + def testPerParallel = testCount.intdiv(parallelCount) + 1 + + for (def x = 0; (x*testPerParallel) < testCount; x++) { + def offset = x + integrationTests["android integration tests: ${offset}"] = { + run: { + runCmdOnDockerImage(imageName, "bash /app/ContainerShip/scripts/run-android-docker-instrumentation-tests.sh --offset=${offset} --count=${testPerParallel}", '--privileged --rm') + } + } + } + + return integrationTests +} + +def runStages() { + def buildInfo = [ + image: [ + name: "facebook/react-native", + tag: null + ], + scm: [ + branch: null, + sha: null, + tag: null, + isPR: false + ] + ] + + node { + def jsDockerBuild, androidDockerBuild + def jsTag, androidTag, jsImageName, androidImageName, parallelInstrumentationTests + + try { + stage('Setup') { + parallel( + 'pull images': { + pullDockerImage('containership/android-base:latest') + } + ) + } + + stage('Build') { + checkout scm + + def githubInfo = calculateGithubInfo() + buildInfo.scm.branch = githubInfo.branch + buildInfo.scm.sha = githubInfo.sha + buildInfo.scm.tag = githubInfo.tag + buildInfo.scm.isPR = githubInfo.isPR + buildInfo.image.tag = "${buildInfo.scm.sha}-${env.BUILD_TAG.replace("/", "-").replace("%2F", "-")}" + + jsTag = "${buildInfo.image.tag}" + androidTag = "${buildInfo.image.tag}" + jsImageName = "${buildInfo.image.name}-js:${jsTag}" + androidImageName = "${buildInfo.image.name}-android:${androidTag}" + + parallelInstrumentationTests = getParallelInstrumentationTests('./ReactAndroid/src/androidTest/java/com/facebook/react/tests', 3, androidImageName) + + parallel( + 'javascript build': { + jsDockerBuild = docker.build("${jsImageName}", "-f ContainerShip/Dockerfile.javascript .") + }, + 'android build': { + androidDockerBuild = docker.build("${androidImageName}", "-f ContainerShip/Dockerfile.android .") + } + ) + + } + + stage('Tests JS') { + parallel( + 'javascript flow': { + runCmdOnDockerImage(jsImageName, 'yarn run flow -- check', '--rm') + }, + 'javascript tests': { + runCmdOnDockerImage(jsImageName, 'yarn test --maxWorkers=4', '--rm') + }, + 'documentation tests': { + runCmdOnDockerImage(jsImageName, 'cd website && yarn test', '--rm') + }, + 'documentation generation': { + runCmdOnDockerImage(jsImageName, 'cd website && node ./server/generate.js', '--rm') + } + ) + } + + stage('Tests Android') { + parallel( + 'android unit tests': { + runCmdOnDockerImage(androidImageName, 'bash /app/ContainerShip/scripts/run-android-docker-unit-tests.sh', '--privileged --rm') + }, + 'android e2e tests': { + runCmdOnDockerImage(androidImageName, 'bash /app/ContainerShip/scripts/run-ci-e2e-tests.sh --android --js', '--rm') + } + ) + } + + stage('Tests Android Instrumentation') { + // run all tests in parallel + parallel(parallelInstrumentationTests) + } + + stage('Cleanup') { + cleanupImage(jsDockerBuild) + cleanupImage(androidDockerBuild) + } + } catch(err) { + cleanupImage(jsDockerBuild) + cleanupImage(androidDockerBuild) + + throw err + } + } + +} + +def isMasterBranch() { + return env.GIT_BRANCH == 'master' +} + +def gitCommit() { + return sh(returnStdout: true, script: 'git rev-parse HEAD').trim() +} + +def cleanupImage(image) { + if (image) { + try { + sh "docker ps -a | awk '{ print \$1,\$2 }' | grep ${image.id} | awk '{print \$1 }' | xargs -I {} docker rm {}" + sh "docker rmi -f ${image.id}" + } catch(e) { + echo "Error cleaning up ${image.id}" + echo "${e}" + } + } +} + +runPipeline() diff --git a/package.json b/package.json index 40861a648..bf92e9ae6 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,16 @@ "test": "jest", "flow": "flow", "lint": "eslint Examples/ Libraries/", - "start": "/usr/bin/env bash -c './packager/packager.sh \"$@\" || true' --" + "start": "/usr/bin/env bash -c './packager/packager.sh \"$@\" || true' --", + "test-android-setup": "docker pull containership/android-base:latest", + "test-android-build": "docker build -t react/android -f ContainerShip/Dockerfile.android .", + "test-android-run-instrumentation": "docker run --cap-add=SYS_ADMIN -it react/android bash ContainerShip/scripts/run-android-docker-instrumentation-tests.sh", + "test-android-run-unit": "docker run --cap-add=SYS_ADMIN -it react/android bash ContainerShip/scripts/run-android-docker-unit-tests.sh", + "test-android-run-e2e": "docker run -it react/android bash ContainerShip/scripts/run-ci-e2e-tests.sh --android --js", + "test-android-all": "npm run test-android-build && npm run test-android-run-unit && npm run test-android-run-instrumentation && npm run test-android-run-e2e", + "test-android-instrumentation": "npm run test-android-build && npm run test-android-run-instrumentation", + "test-android-unit": "npm run test-android-build && npm run test-android-run-unit", + "test-android-e2e": "npm run test-android-build && npm run test-android-run-e2e" }, "bin": { "react-native": "local-cli/wrong-react-native.js"