chore: try to setup CI instrumentation tests

This commit is contained in:
Akash Yadav
2023-10-08 15:22:54 +05:30
parent 8e500547b7
commit 8b7b374a68
7 changed files with 380 additions and 4 deletions
+66 -2
View File
@@ -143,9 +143,73 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: test-reports
name: test-results-unit-tests
path: '**/build/reports/tests/'
instrumentation_test_debug:
runs-on: macos-latest
timeout-minutes: 120
strategy:
fail-fast: false
env:
TERM: dumb
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Change Gradle wrapper permissions
run: chmod +x ./gradlew
- name: Restore cache
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
arch: arm64-v8a
channel: stable
target: google_apis
profile: pixel_6
avd-name: macOs-avd-google_apis-arm64-v8a-33
script: ./.tools/run_instrumentation_tests.sh --log-file=logcat.txt --run-affected --affected-base-ref=$BASE_REF
env:
IDE_INSTRUMENTATION_SETUP_BUILDTOOLS: true
ANDROIDIDE_TEST: true
IDE_SIGNING_ALIAS: ${{ secrets.IDE_SIGNING_ALIAS }}
IDE_SIGNING_AUTH_PASS: ${{ secrets.IDE_SIGNING_AUTH_PASS }}
IDE_SIGNING_AUTH_USER: ${{ secrets.IDE_SIGNING_AUTH_USER }}
IDE_SIGNING_KEY_PASS: ${{ secrets.IDE_SIGNING_KEY_PASS }}
IDE_SIGNING_STORE_PASS: ${{ secrets.IDE_SIGNING_STORE_PASS }}
IDE_SIGNING_URL: ${{ secrets.IDE_SIGNING_URL }}
IDE_SIGNING_KEY_BIN: ${{ secrets.IDE_SIGNING_KEY_BIN }}
- name: Upload logs
if: always()
uses: actions/upload-artifact@v2
with:
name: emulator-logs
path: logcat.txt
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: test-results-instrumentation-tests
path: |
**/build/reports/*
**/build/outputs/*/connected/*
unit_test_tooling_api:
name: Build and test Tooling API
runs-on: ubuntu-latest
@@ -191,7 +255,7 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: tooling-api-test-reports
name: test-results-tooling-api-tests
path: '**/build/reports/tests/'
publish:
+13
View File
@@ -70,6 +70,19 @@
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1679989688">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Tests" value="360" />
<entry key="realme RMX3312" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
#
# This file is part of AndroidIDE.
#
# AndroidIDE is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# AndroidIDE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
#
# Fail on error and print out commands
set -ex
# By default we don't shard
SHARD_COUNT=0
SHARD_INDEX=0
# By default we don't log
LOG_FILE=""
# By default we run tests on device
DEVICE=true
# The task for installing the tools in the emulator
INSTALLER_TEST_TASK_NAME=":app:connectedArm64-v8aDebugAndroidTest"
# Fully qualified name of the class which install installs the build tools
INSTALLER_TEST_CLASS_NAME="com.itsaky.androidide.BuildToolsInstallerTest"
# Common Gradle invocation arguments
GRADLE_INVOCATION_ARGS="--scan --continue --no-configuration-cache --stacktrace"
# Parse parameters
for i in "$@"; do
case $i in
--shard-count=*)
SHARD_COUNT="${i#*=}"
shift
;;
--unit-tests)
DEVICE=false
shift
;;
--shard-index=*)
SHARD_INDEX="${i#*=}"
shift
;;
--log-file=*)
LOG_FILE="${i#*=}"
shift
;;
--run-affected)
RUN_AFFECTED=true
shift
;;
--run-flaky-tests)
RUN_FLAKY=true
shift
;;
--affected-base-ref=*)
BASE_REF="${i#*=}"
shift
;;
*)
echo "Unknown option"
exit 1
;;
esac
done
# Start logcat if we have a file to log to
if [[ -n "$LOG_FILE" ]]; then
adb logcat >"$LOG_FILE" &
fi
FILTER_OPTS=""
# Filter out flaky tests if we're not set to run them
if [[ -z "$RUN_FLAKY" ]]; then
FILTER_OPTS="$FILTER_OPTS -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest"
fi
# If we're set to only run affected test, update the Gradle task
if [[ -n "$RUN_AFFECTED" ]]; then
if [ "$DEVICE" = true ]; then
TASK="runAffectedAndroidTests"
else
TASK="runAffectedUnitTests"
fi
TASK="$TASK -Paffected_module_detector.enable"
# If we have a base branch set, add the Gradle property
if [[ -n "$BASE_REF" ]]; then
TASK="$TASK -Paffected_base_ref=$BASE_REF"
fi
fi
# If we don't have a task yet, use the defaults
if [[ -z "$TASK" ]]; then
if [ "$DEVICE" = true ]; then
TASK="connectedCheck"
else
TASK="testDebug"
fi
fi
SHARD_OPTS=""
if [ "$SHARD_COUNT" -gt "0" ]; then
# If we have a shard count value, create the necessary Gradle property args.
# We assume that SHARD_INDEX has been set too
SHARD_OPTS="$SHARD_OPTS -Pandroid.testInstrumentationRunnerArguments.numShards=$SHARD_COUNT"
SHARD_OPTS="$SHARD_OPTS -Pandroid.testInstrumentationRunnerArguments.shardIndex=$SHARD_INDEX"
fi
# Run the build tools installer test
# shellcheck disable=SC2086
./gradlew \
$GRADLE_INVOCATION_ARGS \
$INSTALLER_TEST_TASK_NAME \
-Pandroid.testInstrumentationRunnerArguments.class=$INSTALLER_TEST_CLASS_NAME
# Run the actual tests
# shellcheck disable=SC2086
./gradlew $GRADLE_INVOCATION_ARGS $TASK $FILTER_OPTS $SHARD_OPTS
@@ -0,0 +1,103 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.R.string
import com.itsaky.androidide.shell.executeProcessAsync
import com.itsaky.androidide.testing.android.clickAndWaitForWindowUpdate
import com.itsaky.androidide.testing.android.findObjectWithText
import com.itsaky.androidide.testing.android.hasObjectWithText
import com.itsaky.androidide.testing.android.launchAndroidIDE
import com.itsaky.androidide.utils.Environment
import org.junit.Test
import org.junit.runner.RunWith
/**
* A special test case which is used to install the build tools on a test device. This is used to
* setup the test device before running further tests.
*
* This class should not define any other tests.
*
* @author Akash Yadav
*/
@RunWith(AndroidJUnit4::class)
class BuildToolsInstallerTest {
companion object {
private const val TAG = "BuildToolsInstallerTest"
}
@Test
fun setupDevice() {
if (!System.getenv("IDE_INSTRUMENTATION_SETUP_BUILDTOOLS").toBoolean()) {
return
}
val device = launchAndroidIDE()
// Allow permissions
while (true) {
val obj = device.findObjectWithText("Allow")
obj?.clickAndWaitForWindowUpdate(device) ?: break
}
// Dismiss the 'AndroidIDE statistics' dialog
if (device.hasObjectWithText(string.title_androidide_statistics)
&& device.hasObjectWithText(string.msg_androidide_statistics)
) {
device.findObjectWithText(string.btn_opt_in).clickAndWaitForWindowUpdate(device)
}
// The 'Warning' dialog will be shown, no need to dismiss it.
// Start the installation process
execProcAndLogOutput("pkg upd")
execProcAndLogOutput("yes | pkg upg") // do not use -y
execProcAndLogOutput("yes | idesetup -c")
// Check if the tools were installed
assertThat(Environment.JAVA.exists()).isTrue()
assertThat(Environment.ANDROID_HOME.exists()).isTrue()
Thread.sleep(5000L)
}
private fun execProcAndLogOutput(cmd: String) {
val process = executeProcessAsync {
redirectErrorStream = true
workingDirectory = null
environment = Environment.getEnvironment()
command = listOf(Environment.SHELL.absolutePath, "-c", cmd).also {
Log.d(TAG, "setupDevice: Full command : ${it.joinToString(separator = " ")}")
}
}
logOutput(cmd, process)
process.waitFor()
}
private fun logOutput(cmd: String, process: Process) {
process.inputStream.bufferedReader().forEachLine {
Log.d(TAG, "setupDevice: '${cmd}': $it")
}
}
}
@@ -0,0 +1,40 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package androidx.test.uiautomator;
import android.app.UiAutomation;
/**
* @author Akash Yadav
*/
public class UiDeviceAccessor {
/**
* Get the {@link UiAutomation} instance from the given {@link UiDevice}.
*/
public static UiAutomation getUiAutomation(UiDevice device) {
return device.getUiAutomation();
}
/**
* Get the {@link InteractionController} instance from the given {@link UiDevice}.
*/
public static InteractionController getInteractionController(UiDevice device) {
return device.getInteractionController();
}
}
@@ -18,6 +18,7 @@
package com.itsaky.androidide.testing.android
import android.app.Activity
import android.app.UiAutomation
import android.content.Intent
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
@@ -25,6 +26,7 @@ import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiDeviceAccessor
import androidx.test.uiautomator.Until
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.app.BaseApplication
@@ -91,3 +93,9 @@ fun <T : Activity> getActivityInStage(stage: Stage): T? {
return activity
}
/**
* @see UiDeviceAccessor.getUiAutomation
*/
val UiDevice.uiAutomation: UiAutomation
get() = UiDeviceAccessor.getUiAutomation(this)
@@ -17,12 +17,18 @@
package com.itsaky.androidide.testing.android
import android.view.KeyCharacterMap
import android.view.KeyEvent
import androidx.annotation.StringRes
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import com.itsaky.androidide.buildinfo.BuildInfo
private val keyCharMap by lazy {
KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD)
}
/**
* @see UiDevice.waitForWindowUpdate
*/
@@ -52,3 +58,16 @@ fun UiDevice.findObjectWithText(text: String): UiObject2? =
*/
fun UiDevice.findObjectWithText(@StringRes text: Int): UiObject2? =
findObject(By.text(stringRes(text)))
/**
* Sends key events to this [UiDevice] to type the given [text].
*/
fun UiDevice.sendKeyEvents(text: String) {
waitForIdle()
val keyEvents = keyCharMap.getEvents(text.toCharArray())
for (keyEvent in keyEvents) {
uiAutomation.injectInputEvent(keyEvent, true)
// pressKeyCode(keyEvent.keyCode, keyEvent.metaState)
waitForIdle()
}
}