diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..4ca0b81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +/.idea +.gradle +/local.properties +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/README.md b/README.md new file mode 100644 index 0000000..96f8a1a --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# Colibri +Colibri is an android library for autotesting UI. + +Uses UiAutomator and Espresso. + +![Colibri](assets/colibri.gif) + +## Gradle Dependency + +Add it in your root build.gradle at the end of repositories: + +````java +allprojects { + repositories { + ... + maven { url "https://jitpack.io"} + } +} +```` + +Add the dependency: + +````java +dependencies { + androidTestImplementation 'com.github.kernel0x.colibri:1.0.0' +} +```` + +## How to use + +In androidTest create a class inheritable from ColibriTest or in already created class initialize class Colibri. + +````java +class SampleColibriTest : ColibriTest() { + override fun getCondition(): Condition { + return Condition.Builder() + .randomInputText(arrayOf("borscht", "vodka", "bear")) + .pause(Duration(500, TimeUnit.MILLISECONDS)) + .build() + } + + override fun getStrategy(): Strategy { + return Monkey() + } + + @Test + fun colibriTest() { + launch() + } +} +```` +OR + +````java +class SampleColibriTest { + + @get:Rule + var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE) + + @Test + fun colibriTest() { + Colibri.condition(Condition.Builder() + .randomInputText(arrayOf("borscht", "vodka", "bear")) + .pause(Duration(500, TimeUnit.MILLISECONDS)).build()) + .strategy(Monkey()) + .launch() + } +} +```` + +Everything is simple. Now you can run the test! + +## How to works + +Colibri runs throughout the app, analyzing UI elements on each screen. + +You can set different testing strategies and conditions. All conditions are available in Condition.Builder() + +You can create custom behavior that will be executed at each step (for example for authorization). Example: +````java +.addCustomBehavior(CustomBehavior { + if (getCurrentActivity().localClassName.equals(LoginActivity::class.java.canonicalName)) { + try { + onView(withId(R.id.text_username)).perform(setTextInEditText("mylogin")) + onView(withId(R.id.text_password)).perform(setTextInEditText("qwerty")) + onView(withId(R.id.button_login)).perform(click()) + Thread.sleep(Duration.FIVE_SECONDS.valueAsMs) + } catch (e: Exception) {} + } + }) +```` + +## Features + +* condition configuration +* different behavioral strategies +* customization of behavior +* logging and screenshots + +## Try it + +Check out the [sample project](/sample) to try it yourself! :wink: + +## Releases + +Checkout the [Releases](https://github.com/kernel0x/colibri/releases) tab for all release info. diff --git a/assets/colibri.gif b/assets/colibri.gif new file mode 100644 index 0000000..b726ee6 Binary files /dev/null and b/assets/colibri.gif differ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..aa90f74 --- /dev/null +++ b/build.gradle @@ -0,0 +1,25 @@ +apply from: 'dependencies.gradle' + +buildscript { + ext.kotlin_version = '1.3.40' + repositories { + jcenter() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.4.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + jcenter() + google() + maven { url 'https://jitpack.io' } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/dependencies.gradle b/dependencies.gradle new file mode 100644 index 0000000..9d9ecc8 --- /dev/null +++ b/dependencies.gradle @@ -0,0 +1,9 @@ +ext.versions = [ + minSdk : 18, + targetSdk : 29, + compileSdk : 29, + buildTools : '29.0.0', + + kotlin_version : '1.3.40' +] + diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 0000000..474de07 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +VERSION_NAME=1.0.0 +VERSION_CODE=1 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e2597c5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jul 15 18:41:36 MSK 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..1c29ed2 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + versionCode 1 + versionName "1.0" + } +} + +dependencies { + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support.test:rules:1.0.2' + implementation 'com.android.support.test:runner:1.0.2' + implementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin_version" +} \ No newline at end of file diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/library/src/androidTest/AndroidManifest.xml b/library/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..dd78e16 --- /dev/null +++ b/library/src/androidTest/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b4b5c2f --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/library/src/main/java/com/kernel/colibri/Colibri.kt b/library/src/main/java/com/kernel/colibri/Colibri.kt new file mode 100644 index 0000000..9441ca9 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/Colibri.kt @@ -0,0 +1,41 @@ +package com.kernel.colibri + +import android.support.test.InstrumentationRegistry +import android.util.Log +import com.kernel.colibri.core.models.Condition +import com.kernel.colibri.core.Config.TAG +import com.kernel.colibri.core.strategy.Monkey +import com.kernel.colibri.core.strategy.Strategy + +object Colibri { + + var packageName = InstrumentationRegistry.getContext().packageName.replace(".test", "") + private var condition = Condition.Builder().build() + private var strategy: Strategy = Monkey() + + fun condition(condition: Condition): Colibri { + this.condition = condition + this.strategy.condition = condition + return this + } + + fun strategy(strategy: Strategy): Colibri { + this.strategy = strategy + this.strategy.condition = condition + return this + } + + fun packageName(packageName: String): Colibri { + this.packageName = packageName + return this + } + + fun launch() { + try { + strategy.run() + } catch (e: IllegalStateException) { + Log.e(TAG, "Don't panic! Something went wrong:" + e.message) + } + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/ColibriTest.kt b/library/src/main/java/com/kernel/colibri/ColibriTest.kt new file mode 100755 index 0000000..3361053 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/ColibriTest.kt @@ -0,0 +1,38 @@ +package com.kernel.colibri + +import android.support.test.rule.GrantPermissionRule +import com.kernel.colibri.core.models.Condition +import com.kernel.colibri.core.Utils.saveLogcat +import com.kernel.colibri.core.strategy.Strategy +import org.junit.After +import org.junit.Rule + +abstract class ColibriTest { + + @get:Rule + var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE) + + abstract fun getCondition(): Condition + + abstract fun getStrategy(): Strategy + + open fun launch() { + Colibri.condition(getCondition()) + .strategy(getStrategy()) + .launch() + } + + open fun launch(packageName: String) { + Colibri.condition(getCondition()) + .strategy(getStrategy()) + .packageName(packageName) + .launch() + } + + @After + @Throws(Exception::class) + fun tearDown() { + saveLogcat() + } + +} diff --git a/library/src/main/java/com/kernel/colibri/core/Config.kt b/library/src/main/java/com/kernel/colibri/core/Config.kt new file mode 100755 index 0000000..27a47ed --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/Config.kt @@ -0,0 +1,25 @@ +package com.kernel.colibri.core + +import android.os.Environment +import com.kernel.colibri.Colibri +import com.kernel.colibri.core.models.Duration +import java.io.File +import java.util.concurrent.TimeUnit + +object Config { + const val TAG = "Colibri" + val FILE_OUTPUT = File(String.format("%s/%s/%s", Environment.getExternalStorageDirectory(), TAG, Colibri.packageName)) + val LOG_FILE_NAME = "$FILE_OUTPUT/$TAG.log" + val TECH_LOG_FILE_NAME = "$FILE_OUTPUT/Performance.csv" + val COMMON_BUTTONS = arrayOf("OK", "Cancel", "Yes", "No") + val IGNORED_ACTIVITY = arrayOf() + val RANDOM_INPUT_TEXT = arrayOf() + val MAX_STEPS = Int.MAX_VALUE + val MAX_DEPTH = 50 + var MAX_RUNTIME = Duration(5, TimeUnit.HOURS) + var MAX_SCREEN_LOOP = Int.MAX_VALUE + val TIME_PAUSE = Duration.ONE_SECOND + val TIME_LAUNCH = Duration.FIVE_SECONDS + var CAPTURE_STEPS = false + val MAX_SCREENSHOTS = 100 +} diff --git a/library/src/main/java/com/kernel/colibri/core/FileLog.kt b/library/src/main/java/com/kernel/colibri/core/FileLog.kt new file mode 100755 index 0000000..412f5a8 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/FileLog.kt @@ -0,0 +1,56 @@ +package com.kernel.colibri.core + +import android.util.Log +import com.kernel.colibri.core.Config.LOG_FILE_NAME +import java.io.BufferedWriter +import java.io.FileWriter +import java.io.IOException +import java.io.PrintWriter +import java.text.SimpleDateFormat +import java.util.* + +object FileLog { + @Synchronized + private fun write(type: Char, tag: String, msg: String) { + val sdf = SimpleDateFormat("MM-dd HH:mm:ss") + var writer: PrintWriter? = null + try { + writer = PrintWriter(BufferedWriter(FileWriter(LOG_FILE_NAME, true))) + writer.println(String.format("%s %c/%s: %s", sdf.format(Date()), type, tag, msg)) + } catch (e: IOException) { + // well now sad + } finally { + writer?.close() + } + } + + @JvmStatic + fun v(tag: String, msg: String): Int { + write('v', tag, msg) + return Log.v(tag, msg) + } + + @JvmStatic + fun w(tag: String, msg: String): Int { + write('w', tag, msg) + return Log.w(tag, msg) + } + + @JvmStatic + fun e(tag: String, msg: String): Int { + write('e', tag, msg) + return Log.e(tag, msg) + } + + @JvmStatic + fun i(tag: String, msg: String): Int { + write('i', tag, msg) + return Log.i(tag, msg) + } + + @JvmStatic + fun d(tag: String, msg: String): Int { + write('d', tag, msg) + return Log.d(tag, msg) + } +} diff --git a/library/src/main/java/com/kernel/colibri/core/Utils.kt b/library/src/main/java/com/kernel/colibri/core/Utils.kt new file mode 100644 index 0000000..39d89aa --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/Utils.kt @@ -0,0 +1,65 @@ +package com.kernel.colibri.core + +import android.app.Activity +import android.support.test.InstrumentationRegistry +import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry +import android.support.test.runner.lifecycle.Stage +import android.view.View +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher +import java.io.File +import java.io.IOException +import java.util.* + +object Utils { + fun getRandomItem(randomText: Array): String { + val rand = Random() + return randomText[rand.nextInt(randomText.size - 1)] + } + + fun getCurrentActivity(): Activity? { + var currentActivity: Activity? = null + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED) + for (activity in resumedActivities) { + currentActivity = activity + break + } + } + return currentActivity + } + + fun toMatcher(v: View): Matcher { + return object : TypeSafeMatcher() { + override fun matchesSafely(item: View): Boolean { + return item === v + } + + override fun describeTo(description: Description) { + description.appendText(v.toString()) + } + } + } + + fun removeAllDirRecursively(fileOrDirectory: File) { + if (fileOrDirectory.isDirectory) + for (child in fileOrDirectory.listFiles()) + removeAllDirRecursively(child) + + fileOrDirectory.delete() + } + + fun saveLogcat() { + val file = File("${Config.FILE_OUTPUT}/${Config.TAG}.Logcat.log") + try { + val newFile = file.createNewFile() + if (newFile) { + val cmd = "logcat -d -s -v time -f " + file.absolutePath + " " + Config.TAG + Runtime.getRuntime().exec(cmd) + } + } catch (e: IOException) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/behaviors/BaseBehavior.kt b/library/src/main/java/com/kernel/colibri/core/behaviors/BaseBehavior.kt new file mode 100644 index 0000000..bcfb4c4 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/behaviors/BaseBehavior.kt @@ -0,0 +1,5 @@ +package com.kernel.colibri.core.behaviors + +abstract class BaseBehavior() : Behavior { + +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/behaviors/Behavior.kt b/library/src/main/java/com/kernel/colibri/core/behaviors/Behavior.kt new file mode 100644 index 0000000..365b6d5 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/behaviors/Behavior.kt @@ -0,0 +1,5 @@ +package com.kernel.colibri.core.behaviors + +interface Behavior { + fun run() +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/behaviors/CustomBehavior.kt b/library/src/main/java/com/kernel/colibri/core/behaviors/CustomBehavior.kt new file mode 100644 index 0000000..d4051ab --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/behaviors/CustomBehavior.kt @@ -0,0 +1,7 @@ +package com.kernel.colibri.core.behaviors + +class CustomBehavior(private val listener: () -> Unit) : BaseBehavior() { + override fun run() { + listener.invoke() + } +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/helpers/ScreenshotHelper.kt b/library/src/main/java/com/kernel/colibri/core/helpers/ScreenshotHelper.kt new file mode 100644 index 0000000..ffe1614 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/helpers/ScreenshotHelper.kt @@ -0,0 +1,45 @@ +package com.kernel.colibri.core.helpers + +import android.support.test.InstrumentationRegistry +import android.support.test.uiautomator.UiDevice +import com.kernel.colibri.core.Config +import com.kernel.colibri.core.Config.FILE_OUTPUT +import com.kernel.colibri.core.Config.TAG +import com.kernel.colibri.core.FileLog +import java.io.File + +object ScreenshotHelper { + var screenshotIndex = 0 + lateinit var lastFilename: String + + fun takeScreenshots(message: String) { + var message = message + if (message.length > 50) { + message = message.substring(0, 49) + } + + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + device.waitForIdle(Config.TIME_PAUSE.valueAsMs) + + var activity = device.currentActivityName ?: "Unknown" + if (activity.length > 30) { + activity = activity.substring(0, 29) + } + + lastFilename = if (message.length > 0) { + String.format("(%d) %s %s.png", + screenshotIndex, toValidFileName(activity), toValidFileName(message)) + } else { + String.format("(%d) %s.png", + screenshotIndex, toValidFileName(activity)) + } + + device.takeScreenshot(File("$FILE_OUTPUT/$lastFilename")) + screenshotIndex++ + FileLog.i(TAG, "{Screenshot} $lastFilename") + } + + fun toValidFileName(input: String?): String { + return input?.replace("[:\\\\/*\"?|<>']".toRegex(), "_") ?: "" + } +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/helpers/UiWatcherHelper.kt b/library/src/main/java/com/kernel/colibri/core/helpers/UiWatcherHelper.kt new file mode 100755 index 0000000..bb4275d --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/helpers/UiWatcherHelper.kt @@ -0,0 +1,125 @@ +package com.kernel.colibri.core.helpers + +import android.support.test.InstrumentationRegistry +import android.support.test.uiautomator.* +import android.util.Log +import com.kernel.colibri.core.Config.TAG +import java.util.* + +class UiWatcherHelper { + + private val errors = ArrayList() + + fun registerAnrAndCrashWatchers() { + device.registerWatcher("ANR") { handleAnr() } + device.registerWatcher("ANR2") { handleAnr2() } + device.registerWatcher("CRASH") { handleCrash() } + device.registerWatcher("CRASH2") { handleCrash2() } + } + + fun handleAnr(): Boolean { + val window = device.findObject(UiSelector() + .className("com.android.server.am.AppNotRespondingDialog")) + var errorText: String? = null + if (window.exists()) { + try { + errorText = window.text + } catch (e: UiObjectNotFoundException) { + // don't care, ignore + } + + onAnrDetected(errorText) + postHandler() + return true + } + return false + } + + fun handleAnr2(): Boolean { + val window = device.findObject(UiSelector().packageName("android") + .textContains("isn't responding.")) + if (window.exists()) { + var errorText: String? = null + try { + errorText = window.text + } catch (e: UiObjectNotFoundException) { + // don't care, ignore + } + + onAnrDetected(errorText) + postHandler() + return true + } + return false + } + + fun handleCrash(): Boolean { + val window = device.findObject(UiSelector() + .className("com.android.server.am.AppErrorDialog")) + if (window.exists()) { + var errorText: String? = null + try { + errorText = window.text + } catch (e: UiObjectNotFoundException) { + // don't care, ignore + } + + onCrashDetected(errorText) + postHandler() + return true + } + return false + } + + fun handleCrash2(): Boolean { + val window = device.findObject(UiSelector().packageName("android") + .textContains("has stopped")) + if (window.exists()) { + var errorText: String? = null + try { + errorText = window.text + } catch (e: UiObjectNotFoundException) { + // don't care, ignore + } + + onCrashDetected(errorText) + postHandler() + return true + } + return false + } + + fun onAnrDetected(errorText: String?) { + errorText?.let { errors.add(it) } + } + + fun onCrashDetected(errorText: String?) { + errorText?.let { errors.add(it) } + } + + fun reset() { + errors.clear() + } + + fun getErrors(): List { + return Collections.unmodifiableList(errors) + } + + fun postHandler() { + val formatedOutput = String.format("UI Exception Message: %-20s\n", device.currentPackageName) + Log.e(TAG, formatedOutput) + + val buttonOK = device.findObject(UiSelector().text("OK").enabled(true)) + + try { + buttonOK.waitForExists(5000) + buttonOK.click() + } catch (e: UiObjectNotFoundException) { + // don't care, ignore + } + } + + companion object { + private var device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/models/Condition.kt b/library/src/main/java/com/kernel/colibri/core/models/Condition.kt new file mode 100644 index 0000000..2f34644 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/models/Condition.kt @@ -0,0 +1,51 @@ +package com.kernel.colibri.core.models + +import com.kernel.colibri.core.Config +import com.kernel.colibri.core.behaviors.CustomBehavior +import java.util.* +import kotlin.collections.ArrayList + +class Condition( + val timePause: Duration, + val randomInputText: Array, + val ignoredActivity: Array, + val maxSteps: Int, + val maxDepth: Int, + val maxRuntime: Duration, + val maxScreenLoop: Int, + val maxScreenshots: Int, + val listCustomBehavior: ArrayList, + val captureSteps: Boolean, + val commonButtons: Array) { + + data class Builder( + private var timePause: Duration = Config.TIME_PAUSE, + private var randomInputText: Array = Config.RANDOM_INPUT_TEXT, + private var ignoredActivity: Array = Config.IGNORED_ACTIVITY, + private var maxSteps: Int = Config.MAX_STEPS, + private var maxDepth: Int = Config.MAX_DEPTH, + private var maxRuntime: Duration = Config.MAX_RUNTIME, + private var maxScreenLoop: Int = Config.MAX_SCREEN_LOOP, + private var maxScreenshots: Int = Config.MAX_SCREENSHOTS, + private var listCustomBehavior: ArrayList = ArrayList(), + private var captureSteps: Boolean = Config.CAPTURE_STEPS, + private var commonButtons: Array = Config.COMMON_BUTTONS) { + + fun pause(timePause: Duration) = apply { this.timePause = timePause } + fun randomInputText(randomInputText: Array) = apply { this.randomInputText = randomInputText } + fun ignoredActivity(ignoredActivity: Array) = apply { this.ignoredActivity = ignoredActivity } + fun maxSteps(maxSteps: Int) = apply { this.maxSteps = maxSteps } + fun maxDepth(maxDepth: Int) = apply { this.maxDepth = maxDepth } + fun maxRuntime(maxRuntime: Duration) = apply { this.maxRuntime = maxRuntime } + fun maxScreenLoop(maxScreenLoop: Int) = apply { this.maxScreenLoop = maxScreenLoop } + fun maxScreenshots(maxScreenshots: Int) = apply { this.maxScreenshots = maxScreenshots } + fun addCustomBehavior(customBehavior: CustomBehavior) = apply { this.listCustomBehavior.add(customBehavior) } + fun captureSteps(captureSteps: Boolean) = apply { this.captureSteps = captureSteps } + fun commonButtons(commonButtons: Array) = apply { this.commonButtons = commonButtons } + fun build() = Condition(timePause, randomInputText, ignoredActivity, maxSteps, maxDepth, maxRuntime, maxScreenLoop, maxScreenshots, listCustomBehavior, captureSteps, commonButtons) + } + + override fun toString(): String { + return "Condition(timePause=$timePause, randomInputText=${Arrays.toString(randomInputText)}, ignoredActivity=${Arrays.toString(ignoredActivity)}, maxSteps=$maxSteps, maxDepth=$maxDepth, maxRuntime=$maxRuntime, maxScreenLoop=$maxScreenLoop, maxScreenshots=$maxScreenshots, listCustomBehavior=$listCustomBehavior, captureSteps=$captureSteps, commonButtons=${Arrays.toString(commonButtons)})" + } +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/models/Duration.kt b/library/src/main/java/com/kernel/colibri/core/models/Duration.kt new file mode 100644 index 0000000..50aec94 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/models/Duration.kt @@ -0,0 +1,206 @@ +package com.kernel.colibri.core.models + +import java.util.concurrent.TimeUnit + +class Duration(val value: Long, val timeUnit: TimeUnit) : Comparable { + + init { + if (value < 0) { + throw IllegalArgumentException("value must be >= 0, was $value") + } + } + + val timeUnitAsString: String + get() = timeUnit.toString().toLowerCase() + + val valueAsMs: Long + get() = if (value == NONE.toLong()) { + value + } else + TimeUnit.MILLISECONDS.convert(value, timeUnit) + + val isForever: Boolean get() = this === FOREVER + + val isZero: Boolean get() = this == ZERO + + fun valueAs(timeUnit: TimeUnit) { + timeUnit.convert(value, timeUnit) + } + + operator fun plus(amount: Long): Duration { + return Plus().apply(this, Duration(amount, timeUnit)) + } + + fun plus(amount: Long, timeUnit: TimeUnit): Duration { + return Plus().apply(this, Duration(amount, timeUnit)) + } + + operator fun plus(duration: Duration): Duration { + return Plus().apply(this, duration) + } + + fun multiply(amount: Long): Duration { + return Multiply().apply(this, Duration(amount, timeUnit)) + } + + fun divide(amount: Long): Duration { + return Divide().apply(this, Duration(amount, timeUnit)) + } + + operator fun minus(amount: Long): Duration { + return Minus().apply(this, Duration(amount, timeUnit)) + } + + fun minus(amount: Long, timeUnit: TimeUnit): Duration { + return Minus().apply(this, Duration(amount, timeUnit)) + } + + operator fun minus(duration: Duration): Duration { + return Minus().apply(this, duration) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val duration = other as Duration + return valueAsMs == duration.valueAsMs + } + + override fun hashCode(): Int { + var result = (value xor value.ushr(32)).toInt() + result = 31 * result + timeUnit.hashCode() + return result + } + + override fun toString(): String { + return "Duration{" + + "timeUnit=" + timeUnit + + ", value=" + value + + '}'.toString() + } + + override fun compareTo(other: Duration): Int { + val x = valueAsMs + val y = other.valueAsMs + return if (x < y) -1 else if (x == y) 0 else 1 + } + + private abstract class BiFunction { + fun apply(lhs: Duration?, rhs: Duration?): Duration { + if (lhs == null || rhs == null) { + throw IllegalArgumentException("Duration cannot be null") + } + + val specialDuration = handleSpecialCases(lhs, rhs) + if (specialDuration != null) { + return specialDuration + } + + val newDuration: Duration + newDuration = when { + lhs.timeUnit.ordinal > rhs.timeUnit.ordinal -> { + val lhsConverted = rhs.timeUnit.convert(lhs.value, lhs.timeUnit) + Duration(apply(lhsConverted, rhs.value), rhs.timeUnit) + } + lhs.timeUnit.ordinal < rhs.timeUnit.ordinal -> { + val rhsConverted = lhs.timeUnit.convert(rhs.value, rhs.timeUnit) + Duration(apply(lhs.value, rhsConverted), lhs.timeUnit) + } + else -> + Duration(apply(lhs.value, rhs.value), lhs.timeUnit) + } + + return newDuration + } + + protected abstract fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? + + internal abstract fun apply(operand1: Long, operand2: Long): Long + } + + private class Plus : BiFunction() { + override fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? { + if (ZERO == rhs) { + return lhs + } else if (ZERO == lhs) { + return rhs + } else if (lhs === FOREVER || rhs === FOREVER) { + return FOREVER + } + return null + } + + override fun apply(operand1: Long, operand2: Long): Long { + return operand1 + operand2 + } + } + + private class Minus : BiFunction() { + override fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? { + if (!lhs.isZero && rhs.isZero) { + return lhs + } else if (lhs === FOREVER) { + return FOREVER + } else if (rhs === FOREVER) { + return ZERO + } else if (FOREVER == rhs) { + return ZERO + } + return null + } + + override fun apply(operand1: Long, operand2: Long): Long { + return operand1 - operand2 + } + } + + private class Multiply : BiFunction() { + override fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? { + if (lhs.isZero || rhs.isZero) { + return ZERO + } else if (lhs === FOREVER || rhs === FOREVER) { + return FOREVER + } + return null + } + + override fun apply(operand1: Long, operand2: Long): Long { + return operand1 * operand2 + } + } + + private class Divide : BiFunction() { + override fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? { + if (lhs === FOREVER) { + return FOREVER + } else if (rhs === FOREVER) { + throw IllegalArgumentException("Cannot divide by infinity") + } else if (ZERO == lhs) { + return ZERO + } + return null + } + + override fun apply(operand1: Long, operand2: Long): Long { + return operand1 / operand2 + } + } + + companion object { + val FOREVER = Duration(java.lang.Long.MAX_VALUE, TimeUnit.DAYS) + val ZERO = Duration(0, TimeUnit.MILLISECONDS) + val ONE_MILLISECOND = Duration(1, TimeUnit.MILLISECONDS) + val ONE_HUNDRED_MILLISECONDS = Duration(100, TimeUnit.MILLISECONDS) + val TWO_HUNDRED_MILLISECONDS = Duration(200, TimeUnit.MILLISECONDS) + val FIVE_HUNDRED_MILLISECONDS = Duration(500, TimeUnit.MILLISECONDS) + val ONE_SECOND = Duration(1, TimeUnit.SECONDS) + val TWO_SECONDS = Duration(2, TimeUnit.SECONDS) + val FIVE_SECONDS = Duration(5, TimeUnit.SECONDS) + val TEN_SECONDS = Duration(10, TimeUnit.SECONDS) + val ONE_MINUTE = Duration(60, TimeUnit.SECONDS) + val TWO_MINUTES = Duration(120, TimeUnit.SECONDS) + val FIVE_MINUTES = Duration(300, TimeUnit.SECONDS) + val TEN_MINUTES = Duration(600, TimeUnit.SECONDS) + private val NONE = -1 + } +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/models/Element.kt b/library/src/main/java/com/kernel/colibri/core/models/Element.kt new file mode 100755 index 0000000..4fe79d6 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/models/Element.kt @@ -0,0 +1,15 @@ +package com.kernel.colibri.core.models + +import android.support.test.uiautomator.UiObject + +/** + * UI element that may be interesting during testing + */ +class Element(var uiObject: UiObject) : Cloneable { + var finished = false + + @Throws(CloneNotSupportedException::class) + override fun clone(): Any { + return super.clone() as Element + } +} diff --git a/library/src/main/java/com/kernel/colibri/core/models/Screen.kt b/library/src/main/java/com/kernel/colibri/core/models/Screen.kt new file mode 100755 index 0000000..9d6b78b --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/models/Screen.kt @@ -0,0 +1,183 @@ +package com.kernel.colibri.core.models + +import android.support.test.InstrumentationRegistry +import android.support.test.uiautomator.* +import android.util.Log +import com.kernel.colibri.Colibri +import com.kernel.colibri.core.Config.TAG +import org.hamcrest.CoreMatchers.notNullValue +import org.junit.Assert.assertThat +import java.util.* + +/** + * UI screen that may be interesting during testing + */ +class Screen { + + lateinit var device: UiDevice + var parentScreen: Screen? = null + var parentElement: Element? = null + var packageName: String = "" + private lateinit var childScreenList: List + private lateinit var rootObject: UiObject + private lateinit var signature: String + lateinit var elementsList: MutableList + lateinit var name: String + var depth = -1 + var id = -1 + var loop = 0 + + private var finished = false + var isFinished: Boolean + get() { + if (finished) + return finished + + for (child in elementsList) { + if (!child.finished) + return false + } + finished = true + return finished + } + set(finished) { + this.finished = finished + + if (this.finished) { + for (child in elementsList) { + child.finished = this.finished + } + } + } + + constructor(parent: Screen?, element: Element?) { + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val root = device.findObject(UiSelector().index(0)) + init(parent, element, root) + } + + constructor(parent: Screen?, element: Element?, root: UiObject) { + init(parent, element, root) + } + + fun init(parent: Screen?, element: Element?, root: UiObject) { + assertThat(root, notNullValue()) + + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + parentScreen = parent + parentElement = element + rootObject = root + childScreenList = ArrayList() + elementsList = ArrayList() + packageName = root.packageName + + signature = "" + name = device.currentActivityName + depth = if (parentScreen == null) 0 else parentScreen!!.depth + 1 + id = -1 + finished = false + + initSignature(rootObject) + + if (!packageName.equals(Colibri.packageName, ignoreCase = true)) { + return + } + + var i = 0 + var clickable: UiObject? + do { + clickable = device.findObject(UiSelector().clickable(true).instance(i++)) + if (clickable != null && clickable.exists()) + elementsList.add(Element(clickable)) + } while (clickable != null && clickable.exists()) + + if (elementsList.size == 0) + finished = true + } + + private fun initSignature(uiObject: UiObject?) { + val conf = Configurator.getInstance() + val waitForIdleTimeout = conf.waitForIdleTimeout + val waitForSelectorTimeout = conf.waitForSelectorTimeout + val actionAcknowledgmentTimeout = conf.actionAcknowledgmentTimeout + val scrollAcknowledgmentTimeout = conf.scrollAcknowledgmentTimeout + conf.waitForIdleTimeout = 0L + conf.waitForSelectorTimeout = 0L + conf.actionAcknowledgmentTimeout = 0L + conf.scrollAcknowledgmentTimeout = 0L + + parseSignature(uiObject) + + conf.waitForIdleTimeout = waitForIdleTimeout + conf.waitForSelectorTimeout = waitForSelectorTimeout + conf.actionAcknowledgmentTimeout = actionAcknowledgmentTimeout + conf.scrollAcknowledgmentTimeout = scrollAcknowledgmentTimeout + } + + private fun parseSignature(uiObject: UiObject?): Boolean { + if (uiObject == null || !uiObject.exists()) + return false + + if (signature.length > 160) + return true + + try { + var classname = "" + for (tmp in uiObject.className.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + classname = tmp + } + signature = "$signature$classname;" + + for (i in 0 until uiObject.childCount) { + parseSignature(uiObject.getChild(UiSelector().index(i))) + } + } catch (e: UiObjectNotFoundException) { + Log.e(TAG, "UiObjectNotFoundException", e) + return false + } + + return true + } + + override fun toString(): String { + var str = "name:" + name + + ", id:" + id + + ", depth:" + depth + + ", finished:" + finished + + ", signature:" + signature + + ", elements:" + elementsList.size + for (i in elementsList.indices) { + val element = elementsList[i] + str += " " + i + ":" + element.finished + } + return str + } + + override fun equals(o: Any?): Boolean { + if (o === this) { + return true + } + if (o !is Screen) { + return false + } + val c = o as Screen? + return this.signature == c!!.signature + } + + override fun hashCode(): Int { + var result = device.hashCode() + result = 31 * result + (parentScreen?.hashCode() ?: 0) + result = 31 * result + (parentElement?.hashCode() ?: 0) + result = 31 * result + packageName.hashCode() + result = 31 * result + childScreenList.hashCode() + result = 31 * result + rootObject.hashCode() + result = 31 * result + signature.hashCode() + result = 31 * result + elementsList.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + depth + result = 31 * result + id + result = 31 * result + loop + result = 31 * result + finished.hashCode() + return result + } +} diff --git a/library/src/main/java/com/kernel/colibri/core/performance/CpuInfo.kt b/library/src/main/java/com/kernel/colibri/core/performance/CpuInfo.kt new file mode 100644 index 0000000..d5e2fc8 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/performance/CpuInfo.kt @@ -0,0 +1,63 @@ +package com.kernel.colibri.core.performance + +import java.io.BufferedReader +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStreamReader + +object CpuInfo { + + private var totalCpuTime1 = -1f + private var processCpuTime1 = -1f + + private val totalCpuTime: Long + get() { + var cpuInfos: Array? = null + try { + val reader = BufferedReader(InputStreamReader(FileInputStream("/proc/stat")), 1000) + val load = reader.readLine() + reader.close() + cpuInfos = load.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + } catch (ex: IOException) { + ex.printStackTrace() + } + + return java.lang.Long.parseLong(cpuInfos!![2]) + java.lang.Long.parseLong(cpuInfos[3]) + java.lang.Long.parseLong(cpuInfos[4]) + java.lang.Long.parseLong(cpuInfos[6]) + java.lang.Long.parseLong(cpuInfos[5]) + java.lang.Long.parseLong(cpuInfos[7]) + java.lang.Long.parseLong(cpuInfos[8]) + } + + fun getProcessCpuRate(pid: Int): Float { + + if (totalCpuTime1 == -1f) { + totalCpuTime1 = totalCpuTime.toFloat() + processCpuTime1 = getAppCpuTime(pid).toFloat() + try { + Thread.sleep(360) + } catch (e: Exception) { + } + } + + val totalCpuTime2 = totalCpuTime.toFloat() + val processCpuTime2 = getAppCpuTime(pid).toFloat() + + val cpuRate = 100 * (processCpuTime2 - processCpuTime1) / (totalCpuTime2 - totalCpuTime1) + + totalCpuTime1 = totalCpuTime2 + processCpuTime1 = processCpuTime2 + + return cpuRate + } + + fun getAppCpuTime(pid: Int): Long { + var cpuInfos: Array? = null + try { + val reader = BufferedReader(InputStreamReader(FileInputStream("/proc/$pid/stat")), 1000) + val load = reader.readLine() + reader.close() + cpuInfos = load.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + } catch (ex: IOException) { + ex.printStackTrace() + } + + return java.lang.Long.parseLong(cpuInfos!![13]) + java.lang.Long.parseLong(cpuInfos[14]) + java.lang.Long.parseLong(cpuInfos[15]) + java.lang.Long.parseLong(cpuInfos[16]) + } +} diff --git a/library/src/main/java/com/kernel/colibri/core/performance/MemInfo.kt b/library/src/main/java/com/kernel/colibri/core/performance/MemInfo.kt new file mode 100644 index 0000000..d9ade26 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/performance/MemInfo.kt @@ -0,0 +1,16 @@ +package com.kernel.colibri.core.performance + +import android.app.ActivityManager +import android.content.Context +import android.support.test.InstrumentationRegistry + +object MemInfo { + fun getProcessMemInfo(pid: Int): android.os.Debug.MemoryInfo { + val context = InstrumentationRegistry.getContext() + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val pids = IntArray(1) + pids[0] = pid + val mems = am.getProcessMemoryInfo(pids) + return mems[0] + } +} diff --git a/library/src/main/java/com/kernel/colibri/core/performance/TechLog.kt b/library/src/main/java/com/kernel/colibri/core/performance/TechLog.kt new file mode 100644 index 0000000..cf5e623 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/performance/TechLog.kt @@ -0,0 +1,147 @@ +package com.kernel.colibri.core.performance + +import android.app.ActivityManager +import android.content.Context +import android.os.Build +import android.support.test.InstrumentationRegistry +import android.util.Log + +import com.kernel.colibri.core.FileLog + +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStreamWriter +import java.io.PrintWriter +import java.text.SimpleDateFormat +import java.util.ArrayList +import java.util.Date + +import android.provider.ContactsContract.Directory.PACKAGE_NAME +import com.kernel.colibri.core.Config.TECH_LOG_FILE_NAME +import com.kernel.colibri.core.Config.TAG + +object TechLog { + + var memList: ArrayList = ArrayList() + var cpuList: ArrayList = ArrayList() + + var cpuLast = 0f + var cpuPeak = 0f + var memLast = 0 + var memPeak = 0 + + val averageCpu: Float + get() { + if (cpuList.size == 0) + return 0f + + var total = 0f + var average = 0f + for (i in cpuList.indices) { + total += cpuList[i] + } + average = total / cpuList.size + return average + } + + val averageMemory: Int + get() { + if (memList.size == 0) + return 0 + + var total = 0 + var average = 0 + for (i in memList.indices) { + total += memList[i] + } + average = total / memList.size + return average + } + + fun reset() { + memList.clear() + cpuList.clear() + memPeak = 0 + memLast = 0 + cpuPeak = 0f + cpuLast = 0f + } + + @Synchronized + fun init() { + reset() + var writer: PrintWriter? = null + try { + writer = PrintWriter(OutputStreamWriter(FileOutputStream(TECH_LOG_FILE_NAME, true), "UTF-8")) + writer.print("\uFEFF") // byte-order marker (BOM) + } catch (e: IOException) { + e.printStackTrace() + } finally { + writer?.close() + } + writeLog("Time,CPU%,Memory(KB),Screen") + } + + @Synchronized + fun writeLog(str: String) { + var writer: PrintWriter? = null + try { + writer = PrintWriter(OutputStreamWriter(FileOutputStream(TECH_LOG_FILE_NAME, true), "UTF-8")) + writer.print(str + "\r\n") + } catch (e: IOException) { + e.printStackTrace() + } finally { + writer?.close() + } + } + + fun record(screenName: String): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val list = InstrumentationRegistry.getInstrumentation().uiAutomation.windows + for (win in list) { + Log.i(TAG, win.javaClass.name) + Log.i(TAG, win.javaClass.simpleName) + Log.i(TAG, win.javaClass.toString()) + } + } + + val context = InstrumentationRegistry.getContext() + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + + val appList = am.runningAppProcesses + for (app in appList) { + if (PACKAGE_NAME.equals(app.processName, ignoreCase = true)) { + + cpuLast = CpuInfo.getProcessCpuRate(app.pid) + cpuList.add(cpuLast) + if (cpuLast > cpuPeak) cpuPeak = cpuLast + + val mem = MemInfo.getProcessMemInfo(app.pid) + memLast = mem.totalPss + memList.add(memLast) + if (mem.totalPss > memPeak) memPeak = mem.totalPss + + val log = String.format("{Tech} package:%s, cpu:%.1f%%" + + ", memory total pss (KB):%d, total private dirty (KB):%d, total shared (KB):%d" + + ", dalvik private:%d, dalvik shared:%d, dalvik pss:%d" + + ", native private:%d, native shared:%d, native pss:%d" + + ", others private:%d, others shared:%d, others pss:%d", + app.processName, cpuLast, + mem.totalPss, mem.totalPrivateDirty, mem.totalSharedDirty, + mem.dalvikPrivateDirty, mem.dalvikSharedDirty, mem.dalvikPss, + mem.nativePrivateDirty, mem.nativeSharedDirty, mem.nativePss, + mem.otherPrivateDirty, mem.otherSharedDirty, mem.otherPss) + + FileLog.i(TAG, log) + + val sdf = SimpleDateFormat("MM-dd HH:mm:ss") + writeLog(String.format("%s,%.1f%%,%d,%s", sdf.format(Date()), cpuLast, memLast, screenName)) + + break + } + } + + return true + } + +} diff --git a/library/src/main/java/com/kernel/colibri/core/strategy/BaseStrategy.kt b/library/src/main/java/com/kernel/colibri/core/strategy/BaseStrategy.kt new file mode 100644 index 0000000..51531be --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/strategy/BaseStrategy.kt @@ -0,0 +1,212 @@ +package com.kernel.colibri.core.strategy + +import android.content.Intent +import android.os.Bundle +import android.support.test.InstrumentationRegistry +import android.support.test.uiautomator.* +import android.util.Log +import com.kernel.colibri.Colibri +import com.kernel.colibri.core.models.Condition +import com.kernel.colibri.core.Config +import com.kernel.colibri.core.Config.TAG +import com.kernel.colibri.core.Config.TIME_LAUNCH +import com.kernel.colibri.core.FileLog +import com.kernel.colibri.core.Utils.removeAllDirRecursively +import com.kernel.colibri.core.helpers.ScreenshotHelper +import com.kernel.colibri.core.helpers.UiWatcherHelper +import com.kernel.colibri.core.models.Screen +import com.kernel.colibri.core.performance.TechLog +import java.util.* + +abstract class BaseStrategy : Strategy { + + override var condition: Condition = Condition.Builder().build() + + private var uiWatcherHelper = UiWatcherHelper() + + val isInTargetApp: Boolean + get() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val pkg = device.currentPackageName + return pkg != null && pkg.equals(Colibri.packageName, ignoreCase = true) + } + + init { + removeAllDirRecursively(Config.FILE_OUTPUT) + if (!Config.FILE_OUTPUT.exists()) { + if (!Config.FILE_OUTPUT.mkdirs()) { + Log.d(TAG, "Failed to create screenshot folder: " + Config.FILE_OUTPUT.path) + } + } + + TechLog.init() + + val conf = Configurator.getInstance() + conf.actionAcknowledgmentTimeout = 200L + conf.scrollAcknowledgmentTimeout = 100L + conf.waitForIdleTimeout = 0L + conf.waitForSelectorTimeout = 0L + logConfiguration() + + uiWatcherHelper.registerAnrAndCrashWatchers() + } + + override fun run() { + launchHome() + pause() + } + + protected fun pause() { + if (!condition.timePause.isZero) { + Thread.sleep(condition.timePause.valueAsMs) + } + } + + fun launchTargetApp(): Boolean { + return launchApp(Colibri.packageName) + } + + private fun launchApp(targetPackage: String): Boolean { + FileLog.i(TAG, "{Launch} $targetPackage") + + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val launcherPackage = device.launcherPackageName + if (launcherPackage.equals(targetPackage, ignoreCase = true)) { + launchHome() + return true + } + val context = InstrumentationRegistry.getContext() + val intent = context.packageManager.getLaunchIntentForPackage(targetPackage) + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + context.startActivity(intent) + device.wait(Until.hasObject(By.pkg(Colibri.packageName).depth(0)), TIME_LAUNCH.valueAsMs) + } else { + val err = String.format("(%s) No launchable Activity.\n", targetPackage) + Log.e(TAG, err) + val bundle = Bundle() + bundle.putString("ERROR", err) + InstrumentationRegistry.getInstrumentation().finish(1, bundle) + } + return true + } + + fun launchHome() { + FileLog.i(TAG, "{Press} Home") + + val uidevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + uidevice.pressHome() + val launcherPackage = uidevice.launcherPackageName + uidevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), TIME_LAUNCH.valueAsMs) + } + + private fun logConfiguration() { + val conf = Configurator.getInstance() + + val log = String.format("ActionAcknowledgmentTimeout:%d," + + " KeyInjectionDelay:%d, " + + "ScrollAcknowledgmentTimeout:%d," + + " WaitForIdleTimeout:%d," + + " WaitForSelectorTimeout:%d", + conf.actionAcknowledgmentTimeout, + conf.keyInjectionDelay, + conf.scrollAcknowledgmentTimeout, + conf.waitForIdleTimeout, + conf.waitForSelectorTimeout) + + FileLog.i(TAG, log) + + FileLog.i(TAG, "TargetPackage: " + Colibri.packageName + ", " + condition.toString()) + } + + fun handleAndroidUi(): Boolean { + when { + uiWatcherHelper.handleAnr() -> { + ScreenshotHelper.takeScreenshots("[ANR]") + return true + } + uiWatcherHelper.handleAnr2() -> { + ScreenshotHelper.takeScreenshots("[ANR]") + return true + } + uiWatcherHelper.handleCrash() -> { + ScreenshotHelper.takeScreenshots("[CRASH]") + return true + } + uiWatcherHelper.handleCrash2() -> { + ScreenshotHelper.takeScreenshots("[CRASH]") + return true + } + else -> { + // something unknown + } + } + + return false + } + + fun handleCommonDialog(): Boolean { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + var button: UiObject? = null + for (keyword in condition.commonButtons) { + button = device.findObject(UiSelector().text(keyword).enabled(true)) + if (button != null && button.exists()) { + break + } + } + try { + if (button != null && button.exists()) { + button.waitForExists(5000) + button.click() + Log.i(TAG, "{Click} " + button.text + " Button succeeded") + return true + } + } catch (e: UiObjectNotFoundException) { + // don't care, ignore + } + + return false + } + + fun setRandomInputTextToEditText() { + if(condition.randomInputText.isEmpty()) return + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + var edit: UiObject? + var i = 0 + do { + edit = device.findObject(UiSelector().className("android.widget.EditText").instance(i++)) + if (edit != null && edit.exists()) { + try { + val rand = Random() + val text = condition.randomInputText[rand.nextInt(condition.randomInputText.size - 1)] + edit.text = text + } catch (e: UiObjectNotFoundException) { + // it happens + } + + } + } while (edit != null && edit.exists()) + } + + fun isIgnoreScreen(screen: Screen): Boolean { + return isIgnoreScreen(screen.name) + } + + fun isIgnoreScreen(activityName: String): Boolean { + condition.ignoredActivity.forEach { + if (it.equals(activityName, true)) return true + } + return false + } + + fun isSameScreen(target: Screen): Boolean { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val root = device.findObject(UiSelector().packageName(Colibri.packageName)) + if (root == null || !root.exists()) { + Log.e(TAG, "Fail to get screen root object") + return false + } + val current = Screen(null, null, root) + return current == target + } +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/strategy/DepthFirst.kt b/library/src/main/java/com/kernel/colibri/core/strategy/DepthFirst.kt new file mode 100755 index 0000000..89d4290 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/strategy/DepthFirst.kt @@ -0,0 +1,333 @@ +package com.kernel.colibri.core.strategy + +import android.support.test.InstrumentationRegistry +import android.support.test.uiautomator.UiDevice +import android.support.test.uiautomator.UiObjectNotFoundException +import android.util.Log +import com.kernel.colibri.Colibri +import com.kernel.colibri.core.Config +import com.kernel.colibri.core.FileLog +import com.kernel.colibri.core.helpers.ScreenshotHelper +import com.kernel.colibri.core.models.Element +import com.kernel.colibri.core.models.Screen +import com.kernel.colibri.core.performance.TechLog +import java.util.* + +open class DepthFirst : BaseStrategy() { + + private lateinit var device: UiDevice + private lateinit var startTime: Date + private var depth = 0 + private var steps = 0 + private var depthPeak = 0 + private var loop = 0 + private var scannedScreenList = ArrayList() + private var rootScreen: Screen? = null + private var lastScreen: Screen? = null + private var lastInteractedElement: Element? = null + private var lastInteractedLog = "" + private var finished = false + + private val isCurrentPackage: Boolean + get() { + val currentScreen = Screen(null, null) + return if (currentScreen.packageName.equals(Colibri.packageName, ignoreCase = true)) isNewScreen(currentScreen) else false + } + + override fun run() { + super.run() + + reset() + + if (!launchTargetApp()) + return + + while (!finished) { + steps++ + + if (condition.listCustomBehavior.size > 0) { + condition.listCustomBehavior.forEach { it.run() } + } + + var currentScreen = Screen(lastScreen, lastInteractedElement) + currentScreen.id = scannedScreenList.size + 1 + + handleCurrentScreen(currentScreen) + + if (!currentScreen.packageName.equals(Colibri.packageName, true)) { + FileLog.i(Config.TAG, "{Inspect} screen, in other package: " + currentScreen.packageName) + handleOtherPackage(currentScreen) + continue + } + + var newScreen = true + for (screen in scannedScreenList) { + if (screen == currentScreen) { + newScreen = false + currentScreen = screen + depth = currentScreen.depth + break + } + } + + if (depth == 0) { + if (rootScreen != null) { + Log.i(Config.TAG, "Root screen changed, may be due to app's coachmarks") + } + rootScreen = currentScreen + } + + if (newScreen) { + handleNewScreen(currentScreen) + loop = 0 + } else { + handleOldScreen(currentScreen) + if (++loop > condition.maxScreenLoop) { + Log.i(Config.TAG, "Reached max old screen loop, re-launch target app") + loop = 0 + launchTargetApp() + continue + } + } + + if (!currentScreen.isFinished) { + var screen: Screen? = currentScreen + do { + if (screen!!.parentElement != null) + screen.parentElement!!.finished = false + if (screen.parentScreen != null) + screen.parentScreen!!.isFinished = false + screen = screen.parentScreen + } while (screen != null) + } + + setRandomInputTextToEditText() + + handleNextElement(currentScreen) + + if (currentScreen.isFinished) { + Log.d(Config.TAG, "Screen[" + currentScreen.id + "] finished") + + var screen: Screen? = currentScreen + do { + if (screen!!.parentElement != null) + screen.parentElement!!.finished = true + if (screen.parentScreen != null) { + if (!screen.parentScreen!!.isFinished) + break + } + screen = screen.parentScreen + } while (screen != null) + + if (currentScreen === rootScreen) { + if (isCurrentPackage) { + lastInteractedElement?.let { it.finished = false } + rootScreen!!.isFinished = false + } else { + FileLog.i(Config.TAG, "{Stop} root screen finished, id:" + rootScreen!!.id) + finished = true + } + } else { + if (isSameScreen(currentScreen)) { + FileLog.i(Config.TAG, "{Click} Back") + device.pressBack() + } + } + } + + if (Date().time - startTime.time > condition.maxRuntime.valueAsMs) { + FileLog.i(Config.TAG, "{Stop} reached max run-time second: " + condition.maxRuntime.value) + finished = true + } + + if (ScreenshotHelper.screenshotIndex >= Config.MAX_SCREENSHOTS - 1) { + FileLog.i(Config.TAG, "{Stop} reached max screenshot files.") + finished = true + } + + if (steps >= condition.maxSteps) { + FileLog.i(Config.TAG, "{Stop} reached max screenshot files.") + finished = true + } + + if (++currentScreen.loop > condition.maxScreenLoop) { + Log.i(Config.TAG, "Reached max screen loop, set screen finished") + currentScreen.isFinished = true + } + } + + FileLog.i(Config.TAG, "Total executed steps:" + steps + + ", peak depth:" + depthPeak + + ", detected screens:" + scannedScreenList.size + + ", screenshot:" + ScreenshotHelper.screenshotIndex) + + val log = String.format("CPU average:%.1f%%, CPU peak:%.1f%%, " + "Memory average (KB):%d, Memory peak (KB):%d", + TechLog.averageCpu, TechLog.cpuPeak, + TechLog.averageMemory, TechLog.memPeak) + FileLog.i(Config.TAG, log) + } + + open fun handleCurrentScreen(currentScreen: Screen) { + TechLog.record(currentScreen.name) + } + + private fun handleNewScreen(currentScreen: Screen) { + FileLog.i(Config.TAG, "{Inspect} NEW screen, $currentScreen") + lastInteractedLog = "" + lastInteractedElement = null + ScreenshotHelper.takeScreenshots("") + + currentScreen.depth = ++depth + if (depth > depthPeak) + depthPeak = depth + + var stop = false + if (isIgnoreScreen(currentScreen.name)) { + FileLog.i(Config.TAG, "{Inspect} screen, in ignored list: " + currentScreen.name) + stop = true + } + if (depth >= condition.maxDepth) { + Log.i(Config.TAG, "Has reached the MaxDepth: " + condition.maxDepth) + stop = true + } + + if (stop) { + currentScreen.elementsList.clear() + currentScreen.isFinished = true + scannedScreenList.add(currentScreen) + FileLog.i(Config.TAG, "{Click} Back") + device.pressBack() + device.waitForIdle(condition.timePause.valueAsMs) + } else { + scannedScreenList.add(currentScreen) + } + } + + private fun handleOldScreen(currentScreen: Screen) { + FileLog.i(Config.TAG, "{Inspect} OLD screen, $currentScreen") + if (condition.captureSteps) { + ScreenshotHelper.takeScreenshots(lastInteractedLog) + lastInteractedLog = "" + lastInteractedElement = null + } + } + + private fun handleNextElement(currentScreen: Screen) { + val element = getNextElement(currentScreen) ?: return + + var classname = "" + var text = "" + var desc = "" + + try { + classname = element.uiObject.className + text = element.uiObject.text + if (text.length > 25) { + text = text.substring(0, 21) + "..." + } + } catch (e: UiObjectNotFoundException) { + // don't care, ignore + } + + try { + desc = element.uiObject.contentDescription + } catch (e: UiObjectNotFoundException) { + // don't care, ignore + } + + try { + val bounds = element.uiObject.bounds + var clazz = "" + for (tmp in classname.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + clazz = tmp + } + lastInteractedLog = when { + text.isNotEmpty() -> String.format("{Click} %s %s %s", text, clazz, bounds.toShortString()) + desc.isNotEmpty() -> String.format("{Click} %s %s %s", desc, clazz, bounds.toShortString()) + else -> String.format("{Click} %s %s", clazz, bounds.toShortString()) + } + + FileLog.i(Config.TAG, lastInteractedLog) + lastScreen = currentScreen + lastInteractedElement = element + element.finished = true + element.uiObject.click() + } catch (e: UiObjectNotFoundException) { + Log.e(Config.TAG, "UiObjectNotFoundException, failed to test a element") + } + + } + + private fun handleOtherPackage(currentScreen: Screen) { + if (isNewScreen(currentScreen)) { + ScreenshotHelper.takeScreenshots("(" + currentScreen.packageName + ")") + currentScreen.elementsList.clear() + currentScreen.isFinished = true + scannedScreenList.add(currentScreen) + } + + lastInteractedLog = "" + lastInteractedElement = null + + if (handleAndroidUi()) { + FileLog.i(Config.TAG, "Handle Android UI succeeded") + } else if (handleCommonDialog()) { + FileLog.i(Config.TAG, "Handle Common UI succeeded") + } else { + // not handle, try back + FileLog.i(Config.TAG, "{Click} Back") + device.pressBack() + if (!isInTargetApp) { + launchTargetApp() + depth = 0 + } + } + } + + private fun getNextElement(currentScreen: Screen): Element? { + if (currentScreen.isFinished) + return null + for (element in currentScreen.elementsList) { + if (!element.uiObject.exists()) { + element.finished = true + continue + } + if (!element.finished) + return element + } + return null + } + + private fun isNewScreen(currentScreen: Screen): Boolean { + for (screen in scannedScreenList) { + if (screen == currentScreen) { + return false + } + } + return true + } + + private fun reset() { + startTime = Date() + depth = 0 + steps = 0 + loop = 0 + depthPeak = 0 + rootScreen = null + lastScreen = null + lastInteractedElement = null + lastInteractedLog = "" + finished = false + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + scannedScreenList = ArrayList() + } + + private fun logAllScreenInfo() { + for (i in scannedScreenList.indices) { + val screen = scannedScreenList[i] + Log.d(Config.TAG, "Screen[" + (i + 1) + "] " + screen.toString()) + } + Log.d(Config.TAG, "Root Screen id: " + rootScreen!!.id) + } +} + diff --git a/library/src/main/java/com/kernel/colibri/core/strategy/Monkey.kt b/library/src/main/java/com/kernel/colibri/core/strategy/Monkey.kt new file mode 100644 index 0000000..3059b27 --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/strategy/Monkey.kt @@ -0,0 +1,10 @@ +package com.kernel.colibri.core.strategy + +import com.kernel.colibri.core.models.Screen + +class Monkey : DepthFirst() { + override fun handleCurrentScreen(currentScreen: Screen) { + super.handleCurrentScreen(currentScreen) + currentScreen.elementsList.shuffle() + } +} \ No newline at end of file diff --git a/library/src/main/java/com/kernel/colibri/core/strategy/Strategy.kt b/library/src/main/java/com/kernel/colibri/core/strategy/Strategy.kt new file mode 100644 index 0000000..097e67b --- /dev/null +++ b/library/src/main/java/com/kernel/colibri/core/strategy/Strategy.kt @@ -0,0 +1,8 @@ +package com.kernel.colibri.core.strategy + +import com.kernel.colibri.core.models.Condition + +interface Strategy { + var condition: Condition + fun run() +} \ No newline at end of file diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100755 index 0000000..796b96d --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..a301ac8 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + applicationId "com.kernel.colibri.sample" + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation 'com.android.support:appcompat-v7:28.0.0' + androidTestImplementation 'com.android.support.test:rules:1.0.2' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3' + androidTestImplementation 'com.github.kernel0x:colibri:1.0.0' + //androidTestImplementation project(':library') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100755 index 0000000..e69de29 diff --git a/sample/src/androidTest/java/com/kernel/colibri/Sample2ColibriTest.kt b/sample/src/androidTest/java/com/kernel/colibri/Sample2ColibriTest.kt new file mode 100644 index 0000000..c6383c9 --- /dev/null +++ b/sample/src/androidTest/java/com/kernel/colibri/Sample2ColibriTest.kt @@ -0,0 +1,31 @@ +package com.kernel.colibri + +import android.support.test.filters.LargeTest +import android.support.test.rule.GrantPermissionRule +import android.support.test.runner.AndroidJUnit4 +import com.kernel.colibri.core.models.Condition +import com.kernel.colibri.core.models.Duration +import com.kernel.colibri.core.strategy.Monkey +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit + +@LargeTest +@RunWith(AndroidJUnit4::class) +class Sample2ColibriTest { + + @get:Rule + var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE) + + @Test + fun colibriTest() { + Colibri.condition(Condition.Builder() + .randomInputText(arrayOf("borscht", "vodka", "bear")) + .pause(Duration(500, TimeUnit.MILLISECONDS)) + .build()) + .strategy(Monkey()) + .packageName("com.google.android.youtube") + .launch() + } +} diff --git a/sample/src/androidTest/java/com/kernel/colibri/SampleColibriTest.kt b/sample/src/androidTest/java/com/kernel/colibri/SampleColibriTest.kt new file mode 100644 index 0000000..f73fd36 --- /dev/null +++ b/sample/src/androidTest/java/com/kernel/colibri/SampleColibriTest.kt @@ -0,0 +1,31 @@ +package com.kernel.colibri + +import android.support.test.filters.LargeTest +import android.support.test.runner.AndroidJUnit4 +import com.kernel.colibri.core.models.Condition +import com.kernel.colibri.core.models.Duration +import com.kernel.colibri.core.strategy.Monkey +import com.kernel.colibri.core.strategy.Strategy +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit + +@LargeTest +@RunWith(AndroidJUnit4::class) +class SampleColibriTest : ColibriTest() { + override fun getCondition(): Condition { + return Condition.Builder() + .randomInputText(arrayOf("borscht", "vodka", "bear")) + .pause(Duration(500, TimeUnit.MILLISECONDS)) + .build() + } + + override fun getStrategy(): Strategy { + return Monkey() + } + + @Test + fun colibriTest() { + launch("com.google.android.youtube") + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100755 index 0000000..383b6ce --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/com/kernel/colibri/sample/MainActivity.kt b/sample/src/main/java/com/kernel/colibri/sample/MainActivity.kt new file mode 100644 index 0000000..2e04133 --- /dev/null +++ b/sample/src/main/java/com/kernel/colibri/sample/MainActivity.kt @@ -0,0 +1,12 @@ +package com.kernel.colibri.sample + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..601033c --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000..7abc06d --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1 @@ + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..52baf7e --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':sample', ':library'