├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── appunite │ │ │ └── firebasetestlabplugin │ │ │ ├── FirebaseTestLabPlugin.kt │ │ │ ├── FirebaseTestLabPluginExtension.kt │ │ │ ├── cloud │ │ │ ├── CloudTestResultDownloader.kt │ │ │ └── FirebaseTestLabProcessCreator.kt │ │ │ ├── model │ │ │ ├── Device.kt │ │ │ ├── ResultTypes.kt │ │ │ ├── ScreenOrientation.kt │ │ │ └── TestResults.kt │ │ │ ├── tasks │ │ │ ├── FirebaseTestLabProcess.kt │ │ │ └── InstrumentationShardingTask.kt │ │ │ └── utils │ │ │ ├── CloudUtils.kt │ │ │ └── Constants.kt │ └── resources │ │ └── blank.apk │ └── test │ ├── java │ └── com │ │ └── appunite │ │ └── firebasetestlabplugin │ │ ├── ApplicationIntegrationTest.kt │ │ └── LibraryIntegrationTest.kt │ └── resources │ └── com │ └── appunite │ └── firebasetestlabplugin │ └── simple │ ├── key.json │ └── src │ └── main │ └── AndroidManifest.xml ├── sample ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── appunite │ │ │ └── firebaselabplugin │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── appunite │ │ │ │ └── firebaselabplugin │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── appunite │ │ └── firebaselabplugin │ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library │ ├── .gitignore │ ├── build.gradle │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── appunite │ │ │ └── firebaselabplugin │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── appunite │ │ │ └── firebaselabplugin │ │ │ └── MainActivity.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── appunite │ │ └── firebaselabplugin │ │ └── ExampleUnitTest.kt ├── proguard-rules.pro └── settings.gradle └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #Secrets 2 | /id_rsa 3 | /id_rsa.pub 4 | #IntelliJ IDEA 5 | /.idea/** 6 | *.iml 7 | 8 | # Gradle 9 | /.gradle/** 10 | /.android/** 11 | /local.properties 12 | /build 13 | /com 14 | /api/build 15 | /gen-libs/build 16 | /captures 17 | /projectFilesBackup 18 | local.properties 19 | gradle.properties 20 | 21 | # General 22 | .DS_Store 23 | /.gradle/ 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Repository is no longer maintained. 2 | 3 | # Firebase Test Lab Plugin for Android 4 | [![Plugin version badge](https://img.shields.io/static/v1.svg?label=plugin&message=2.6.2&color=blue)](https://plugins.gradle.org/plugin/firebase.test.lab) 5 | [![License](https://img.shields.io/crates/l/rustc-serialize.svg)](https://github.com/piotrmadry/FirebaseTestLab-Android/blob/master/LICENSE) 6 | 7 | ## Introduction 8 | Plugin for which integrates Firebase Test Lab with Android Project. Simplify running Android Tests on Firebase platform locally as well as on using Continuous integration. 9 | 10 | ### Contributors 11 | - [Jacek Marchwicki](https://github.com/jacek-marchwicki) 12 | - [Zbynek Konieczny] (https://github.com/zbynek) 13 | 14 | #### Available features 15 | 16 | - Automatic installation of `gcloud` command line tool 17 | - Creating tasks for testable `buildType`[By default it is `debug`. If you want to change it use `testBuildType "buildTypeName"`] 18 | - Creating tasks for every defined device and configuration separately [ including Instrumented / Robo tests ] 19 | - Creating tasks which runs all configurations at once 20 | - Ability to download tests results to specific location 21 | - Ability to clear directory inside bucket before test run 22 | - Instrumented tests sharding 23 | 24 | #### Benefits 25 | 26 | - Readability 27 | - Simplicity 28 | - Remote and Local Testing 29 | - Compatible with Gradle 3.0 30 | - Instrumented Tests sharding for parallel test execution 31 | 32 | #### Setup 33 | 34 | 1. If you don't have a Firebase project for your app, go to the [Firebase console](https://console.firebase.google.com/) and click Create New Project to create one now. You will need ownership or edit permissions in your project. 35 | 2. Create a service account related with your firebase project with an Editor role in the [Google Cloud Platform console - IAM/Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts/) 36 | 3. Copy `Project ID` from [Google Cloud Platform console - HOME](https://console.cloud.google.com/home) 37 | 4. Add plugin to your root project `build.gradle`: 38 | ```grovy 39 | buildscript { 40 | repositories { 41 | maven { 42 | url "https://plugins.gradle.org/m2/" 43 | } 44 | } 45 | dependencies { 46 | classpath "firebase.test.lab:plugin:X.X.X" 47 | } 48 | } 49 | ``` 50 | 5. Add configuration in your project `build.gradle`: 51 | ```groovy 52 | apply plugin: 'firebase.test.lab' 53 | 54 | firebaseTestLab { 55 | keyFile = file("test-lab-key.json") 56 | googleProjectId = "your-project-app-id" 57 | devices { 58 | nexusEmulator { 59 | deviceIds = ["hammerhead"] 60 | androidApiLevels = [23] 61 | } 62 | } 63 | } 64 | ``` 65 | List of available [devices](https://firebase.google.com/docs/test-lab/images/gcloud-device-list.png) 66 | 6. Run your instrumentations tests 67 | 68 | ```bash 69 | ./gradlew firebaseTestLabExecuteDebugInstrumentation 70 | ``` 71 | 72 | Or run robo tests 73 | 74 | ```bash 75 | ./gradlew firebaseTestLabExecuteDebugRobo 76 | ``` 77 | 78 | #### Advanced configuration 79 | 80 | You can 81 | ``` Goovy 82 | // Setup firebase test lab plugin 83 | firebaseTestLab { 84 | // REQUIRED obtain service key as described inside README 85 | keyFile = file("test-lab-key.json") 86 | // REQUIRED setup google project id ad described inside README 87 | googleProjectId = "your-project-app-id" 88 | 89 | // If you want you can ignore test failures 90 | // ignoreFailures = true 91 | 92 | // If you prefer you can use your custom google storage bucket for storing build sources and results 93 | // cloudBucketName = "your-custome-google-storage-bucket-name" 94 | // If not specified default is: a timestamp with a random suffix 95 | // cloudDirectoryName = "your-custome-directory-name" 96 | 97 | // If you prefer to install gcloud tool manually you can set path by 98 | // cloudSdkPath = "/user/cloud-sdk/bin" 99 | 100 | // If you want to change default gcloud installation path (default is in build/gcloud directory) 101 | // you can set environment variable `export CLOUDSDK_INSTALL_DIR=`/cache/your_directory/` 102 | 103 | // REQUIRED 104 | devices { 105 | // REQUIRED add at least one device 106 | nexusEmulator { 107 | // REQUIRED Choose at least one device id 108 | // you can list all available via `gcloud firebase test android models list` or look on https://firebase.google.com/docs/test-lab/images/gcloud-device-list.png 109 | deviceIds = ["hammerhead"] 110 | 111 | // REQUIRED Choose at least one API level 112 | // you can list all available via `gcloud firebase test android models list` for your device model 113 | androidApiLevels = [23] 114 | 115 | // You can test app in landscape and portrait 116 | // screenOrientations = [com.appunite.firebasetestlabplugin.model.ScreenOrientation.PORTRAIT, com.appunite.firebasetestlabplugin.model.ScreenOrientation.LANDSCAPE] 117 | 118 | // Choose language (default is `en`) 119 | // you can list all available via `gcloud firebase test android locales list` 120 | // locales = ["en"] 121 | 122 | // If you are using ABI splits you can filter selected abi 123 | // filterAbiSplits = true 124 | // abiSplits = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"] 125 | 126 | // If you are using ABI splits you can remove testing universal APK 127 | // testUniversalApk = false 128 | 129 | // For instrumented test you can specify number of shards, which allows to split all the tests for [numShards] times and execute them in parallel 130 | // numShards = 4 131 | 132 | // You can set timeout (in seconds) for test 133 | // timeout = 6000 134 | 135 | // Enable Android Test Orchestrator more info at: https://developer.android.com/training/testing/junit-runner 136 | // isUseOrchestrator = true // default false 137 | 138 | // A list of one or more test target filters to apply (default: run all test targets) 139 | // testTargets = ["size large"] 140 | 141 | // Environment variables are mirrored as extra options to the am instrument -e KEY1 VALUE1 142 | // environmentVariables = ["clearPackageData=true", "coverage=true"] 143 | 144 | // The fully-qualified class name of the instrumentation test runner 145 | // testRunnerClass = "com.my.package.MyRunner" 146 | 147 | // Pass any custom param for gcloud 148 | // customParamsForGCloudTool = --no-performance-metrics 149 | } 150 | // You can define more devices 151 | someOtherDevices { 152 | deviceIds = ["shamu", "flounder"] 153 | androidApiLevels = [21] 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | For more precise test selection run 160 | 161 | ```bash 162 | ./gradlew tasks 163 | ``` 164 | 165 | to discover all available test options 166 | 167 | 168 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.6.21' 5 | repositories { 6 | maven {url "https://plugins.gradle.org/m2/"} 7 | google() 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath "org.gradle.kotlin:gradle-kotlin-dsl-plugins:2.3.3" 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | classpath "org.gradle.kotlin:plugins:1.3.6" 14 | classpath "com.gradle.publish:plugin-publish-plugin:1.0.0" 15 | classpath "com.android.tools.build:gradle-api:7.2.2" 16 | classpath "com.android.tools.build:gradle:7.2.2" 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | maven { url "https://plugins.gradle.org/m2/" } 25 | maven { url "https://repo.gradle.org/gradle/libs-releases-local/" } 26 | } 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Sep 10 13:32:45 CEST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /plugin/.gitignore: -------------------------------------------------------------------------------- 1 | #Secrets 2 | /id_rsa 3 | /id_rsa.pub 4 | #IntelliJ IDEA 5 | /.idea/** 6 | *.iml 7 | 8 | # Gradle 9 | /.gradle/** 10 | /.android/** 11 | /local.properties 12 | gradle.properties 13 | /build 14 | /com 15 | /api/build 16 | /gen-libs/build 17 | /captures 18 | /projectFilesBackup 19 | 20 | # General 21 | .DS_Store 22 | 23 | # Binaries 24 | /out/** 25 | /.gradle/ 26 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "java" 2 | apply plugin: "kotlin" 3 | apply plugin: "java-gradle-plugin" 4 | apply plugin: "maven-publish" 5 | apply plugin: "com.gradle.plugin-publish" 6 | apply plugin: "org.gradle.kotlin.kotlin-dsl" 7 | 8 | group = "firebase.test.lab" 9 | version = "2.6.4" 10 | 11 | pluginBundle { 12 | website = "https://github.com/piotrmadry/firebase-test-lab-gradle-plugin" 13 | vcsUrl = "https://github.com/piotrmadry/firebase-test-lab-gradle-plugin.git" 14 | tags = ["firebase", "test-lab", "espresso", "instrumental-tests", "kotlin", "android"] 15 | } 16 | 17 | gradlePlugin { 18 | plugins { 19 | FirebaseTestLabPlugin { 20 | id = "firebase.test.lab" 21 | displayName = "Firebase Test Lab Plugin" 22 | description = "Gradle plugin for Android Test Lab" 23 | implementationClass = "com.appunite.firebasetestlabplugin.FirebaseTestLabPlugin" 24 | } 25 | } 26 | } 27 | 28 | jar { 29 | manifest { 30 | attributes( 31 | 'Build-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ss.SSSZ"), 32 | 'Build-Revision': project.version, 33 | 'Built-By': 'piotrmadry', 34 | 'Created-By': "Gradle ${gradle.gradleVersion}", 35 | 'Build-Jdk': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})", 36 | ) 37 | } 38 | } 39 | 40 | java { 41 | sourceCompatibility = JavaVersion.VERSION_11 42 | targetCompatibility = JavaVersion.VERSION_11 43 | withJavadocJar() 44 | withSourcesJar() 45 | } 46 | 47 | tasks { 48 | test { 49 | testLogging.showExceptions = true 50 | } 51 | } 52 | 53 | configurations.all { 54 | resolutionStrategy.eachDependency { DependencyResolveDetails details -> 55 | def requested = details.requested 56 | if (requested.group == 'org.jetbrains.kotlin' && requested.name == 'kotlin-reflect') { 57 | details.useVersion kotlin_version 58 | } 59 | } 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 64 | implementation "com.android.tools.build:gradle:3.6.1" 65 | testImplementation "junit:junit-dep:4.4" 66 | } -------------------------------------------------------------------------------- /plugin/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /plugin/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'firebase-test-lab' -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/FirebaseTestLabPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin 2 | 3 | import com.android.build.VariantOutput 4 | import com.android.build.gradle.AppExtension 5 | import com.android.build.gradle.LibraryExtension 6 | import com.android.build.gradle.TestedExtension 7 | import com.android.build.gradle.api.ApkVariant 8 | import com.android.build.gradle.api.ApplicationVariant 9 | import com.android.build.gradle.api.BaseVariantOutput 10 | import com.android.build.gradle.api.ApkVariantOutput 11 | import com.android.build.gradle.api.TestVariant 12 | import com.appunite.firebasetestlabplugin.cloud.CloudTestResultDownloader 13 | import com.appunite.firebasetestlabplugin.cloud.FirebaseTestLabProcessCreator 14 | import com.appunite.firebasetestlabplugin.cloud.ProcessData 15 | import com.appunite.firebasetestlabplugin.cloud.TestType 16 | import com.appunite.firebasetestlabplugin.model.Device 17 | import com.appunite.firebasetestlabplugin.model.TestResults 18 | import com.appunite.firebasetestlabplugin.tasks.InstrumentationShardingTask 19 | import com.appunite.firebasetestlabplugin.utils.Constants 20 | import org.apache.tools.ant.taskdefs.condition.Os 21 | import org.gradle.api.GradleException 22 | import org.gradle.api.Plugin 23 | import org.gradle.api.Project 24 | import org.gradle.api.Task 25 | import org.gradle.api.logging.LogLevel 26 | import org.gradle.api.tasks.Exec 27 | import org.gradle.kotlin.dsl.closureOf 28 | import org.gradle.kotlin.dsl.register 29 | import java.io.BufferedInputStream 30 | import java.io.ByteArrayOutputStream 31 | import java.io.File 32 | import java.io.FileOutputStream 33 | import java.io.IOException 34 | import java.io.Serializable 35 | 36 | 37 | class FirebaseTestLabPlugin : Plugin { 38 | 39 | open class HiddenExec : Exec() { 40 | init { 41 | standardOutput = ByteArrayOutputStream() 42 | errorOutput = standardOutput 43 | isIgnoreExitValue = true 44 | 45 | doLast { 46 | execResult?.let { 47 | if (it.exitValue != 0) { 48 | println(standardOutput.toString()) 49 | throw GradleException("exec failed; see output above") 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | companion object { 57 | private const val GRADLE_METHOD_NAME = "firebaseTestLab" 58 | private const val ANDROID = "android" 59 | private const val ensureGCloudSdk = "firebaseTestLabEnsureGCloudSdk" 60 | private const val taskAuth = "firebaseTestLabAuth" 61 | private const val taskSetup = "firebaseTestLabSetup" 62 | private const val taskSetProject = "firebaseTestLabSetProject" 63 | private const val taskPrefixDownload = "firebaseTestLabDownload" 64 | private const val taskPrefixExecute = "firebaseTestLabExecute" 65 | private const val BLANK_APK_RESOURCE_PATH = "/blank.apk" 66 | 67 | private val BLANK_APK_RESOURCE = FirebaseTestLabPlugin::class.java.getResource(BLANK_APK_RESOURCE_PATH) 68 | } 69 | 70 | private lateinit var project: Project 71 | 72 | /** 73 | * Create extension used to configure testing properties, platforms.. 74 | * After that @param[setup] check for required fields validity 75 | * and throw @param[GradleException] if needed 76 | */ 77 | override fun apply(project: Project) { 78 | this.project = project 79 | project.extensions.create( 80 | GRADLE_METHOD_NAME, 81 | FirebaseTestLabPluginExtension::class.java, 82 | project) 83 | 84 | project.afterEvaluate { 85 | logger.lifecycle("*************** Firebase Test Lab Plugin ***************") 86 | setup() 87 | logger.lifecycle("********************************************************") 88 | } 89 | } 90 | 91 | data class Sdk(val gcloud: File, val gsutil: File): Serializable 92 | 93 | private fun createDownloadSdkTask(project: Project, cloudSdkPath: String?): Sdk = 94 | if (cloudSdkPath != null) { 95 | val sdkPath = File(cloudSdkPath) 96 | val gcloud = File(sdkPath, Constants.GCLOUD) 97 | val gsutil = File(sdkPath, Constants.GSUTIL) 98 | 99 | project.tasks.register(ensureGCloudSdk) { 100 | group = Constants.FIREBASE_TEST_LAB 101 | description = "Check if google cloud sdk is installed" 102 | 103 | doFirst { 104 | if (!gcloud.exists()) { 105 | throw IllegalStateException("gcloud does not exist in path ${sdkPath.absoluteFile}, but downloading is not supported on Windows") 106 | } 107 | if (!gsutil.exists()) { 108 | throw IllegalStateException("gsutil does not exist in path ${sdkPath.absoluteFile}, but downloading is not supported on Windows") 109 | } 110 | } 111 | } 112 | Sdk(gcloud, gsutil) 113 | } else { 114 | val env = System.getenv("CLOUDSDK_INSTALL_DIR") 115 | val installDir = when { 116 | !env.isNullOrEmpty() -> File(env) 117 | else -> File(project.buildDir, "gcloud") 118 | } 119 | 120 | project.logger.lifecycle("Google Cloud SDK installed at: $installDir") 121 | val cloudSdkDir = File(installDir, "google-cloud-sdk") 122 | val sdkPath = File(cloudSdkDir, "bin") 123 | 124 | val gcloud = File(sdkPath, Constants.GCLOUD) 125 | val gsutil = File(sdkPath, Constants.GSUTIL) 126 | 127 | project.tasks.register(ensureGCloudSdk) { 128 | group = Constants.FIREBASE_TEST_LAB 129 | description = "Install google cloud SDK if necessary" 130 | 131 | outputs.files(gcloud, gsutil) 132 | doFirst { 133 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 134 | throw IllegalStateException("Fetching gcloud and gsutil is not supported on Windows. " + 135 | "You need to install it manually. Look for instructions: https://cloud.google.com/sdk/downloads#windows ." + 136 | "Than you need to set:\n " + 137 | "firebaseTestLab {\n" + 138 | " cloudSdkPath = \"Xyz\"\n" + 139 | "}\n") 140 | } 141 | } 142 | commandLine = listOf("bash", "-c", "rm -r \"${cloudSdkDir.absolutePath}\";export CLOUDSDK_CORE_DISABLE_PROMPTS=1 && export CLOUDSDK_INSTALL_DIR=\"${installDir.absolutePath}\" && curl https://sdk.cloud.google.com | bash") 143 | doLast { 144 | if (!gcloud.exists()) throw IllegalStateException("Installation failed") 145 | if (!gsutil.exists()) throw IllegalStateException("Installation failed") 146 | } 147 | } 148 | Sdk(gcloud, gsutil) 149 | } 150 | 151 | sealed class ExtensionType(open val testVariant: TestVariant) { 152 | data class Library(override val testVariant: TestVariant) : ExtensionType(testVariant) 153 | data class Application(override val testVariant: TestVariant, val appVariant: ApplicationVariant) : ExtensionType(testVariant) 154 | } 155 | 156 | private fun setup() { 157 | project.extensions.findByType(FirebaseTestLabPluginExtension::class.java)?.apply { 158 | val devices = devices.toList() 159 | 160 | val sdk = createDownloadSdkTask(project, cloudSdkPath) 161 | 162 | project.tasks.register(taskAuth) { 163 | group = Constants.FIREBASE_TEST_LAB 164 | description = "Authorize google cloud sdk" 165 | 166 | dependsOn(ensureGCloudSdk) 167 | val keyFile = keyFile 168 | doFirst { 169 | if (keyFile == null) { 170 | throw GradleException("You need to set firebaseTestLab.keyFile = file(\"key-file.json\") before run") 171 | } else if (!keyFile.exists()) { 172 | throw GradleException("Key file (${keyFile.absolutePath} does not exists") 173 | } 174 | } 175 | commandLine = listOf(sdk.gcloud.absolutePath, "auth", "activate-service-account", "--key-file=${keyFile?.absolutePath}") 176 | } 177 | project.tasks.register(taskSetProject) { 178 | group = Constants.FIREBASE_TEST_LAB 179 | description = "Configure google cloud sdk project" 180 | 181 | dependsOn(ensureGCloudSdk) 182 | doFirst { 183 | if (googleProjectId == null) { 184 | throw GradleException("You need to set firebaseTestLab.googleProjectId before run") 185 | } 186 | } 187 | commandLine = listOf(sdk.gcloud.absolutePath, "config", "set", "project", "$googleProjectId") 188 | } 189 | project.tasks.register(taskSetup) { 190 | group = Constants.FIREBASE_TEST_LAB 191 | description = "Setup and configure google cloud sdk" 192 | 193 | dependsOn(taskSetProject) 194 | dependsOn(taskAuth) 195 | } 196 | 197 | 198 | val downloader: CloudTestResultDownloader? = if (cloudBucketName != null && cloudDirectoryName != null) { 199 | CloudTestResultDownloader( 200 | sdk, 201 | resultsTypes, 202 | File(cloudDirectoryName!!), 203 | File(project.buildDir, cloudDirectoryName!!), 204 | cloudBucketName!!, 205 | project.logger 206 | ) 207 | } else { 208 | null 209 | } 210 | 211 | if (clearDirectoryBeforeRun && downloader == null) { 212 | throw IllegalStateException("If you want to clear directory before run you need to setup cloudBucketName and cloudDirectoryName") 213 | } 214 | 215 | val androidExtension: Any? = project.extensions.findByName(ANDROID) 216 | 217 | (androidExtension as TestedExtension).apply { 218 | testVariants.toList().forEach { testVariant -> 219 | 220 | val extensionType: ExtensionType = when (androidExtension) { 221 | is LibraryExtension -> ExtensionType.Library(testVariant) 222 | is AppExtension -> ExtensionType.Application(testVariant, androidExtension.applicationVariants.toList().firstOrNull { it.buildType == testVariant.buildType && it.flavorName == testVariant.flavorName }!!) 223 | else -> throw IllegalStateException("Only application and library modules are supported") 224 | } 225 | 226 | createGroupedTestLabTask(devices, extensionType, ignoreFailures, downloader, sdk, cloudBucketName, cloudDirectoryName) 227 | } 228 | } 229 | } 230 | } 231 | 232 | data class DeviceAppMap(val device: Device, val apk: BaseVariantOutput) 233 | 234 | data class Test(val device: Device, val apk: BaseVariantOutput, val testApk: BaseVariantOutput): Serializable 235 | 236 | private fun createGroupedTestLabTask( 237 | devices: List, 238 | extension: ExtensionType, 239 | ignoreFailures: Boolean, 240 | downloader: CloudTestResultDownloader?, 241 | sdk: Sdk, 242 | cloudBucketName: String?, 243 | cloudDirectoryName: String? 244 | ) { 245 | val blankApk = createBlankApkForLibrary(project) 246 | val variantName = extension.testVariant.testedVariant?.name?.capitalize() ?: "" 247 | 248 | val cleanTask = "firebaseTestLabClean${variantName.capitalize()}" 249 | 250 | val variantSuffix = variantName.capitalize() 251 | val runTestsTask = taskPrefixExecute + variantSuffix 252 | val runTestsTaskInstrumentation = "${runTestsTask}Instrumentation" 253 | val runTestsTaskRobo = "${runTestsTask}Robo" 254 | 255 | if (downloader != null) { 256 | project.task(cleanTask, closureOf { 257 | group = Constants.FIREBASE_TEST_LAB 258 | description = "Clean test lab artifacts on google storage" 259 | dependsOn(taskSetup) 260 | doLast { 261 | downloader.clearResultsDir() 262 | } 263 | }) 264 | } 265 | 266 | val appVersions = combineAll(devices, extension.testVariant.testedVariant.outputs, ::DeviceAppMap) 267 | .filter { 268 | val hasAbiSplits = it.apk.filterTypes.contains(VariantOutput.ABI) 269 | if (hasAbiSplits) { 270 | if (it.device.filterAbiSplits) { 271 | val abi = it.apk.filters.first { it.filterType == VariantOutput.ABI }.identifier 272 | it.device.abiSplits.contains(abi) 273 | } else { 274 | true 275 | } 276 | } else { 277 | it.device.testUniversalApk 278 | } 279 | } 280 | 281 | /* Not applicable for library module */ 282 | 283 | val roboTasks = if (extension is ExtensionType.Library) emptyList() else { 284 | appVersions 285 | .map { test -> 286 | val devicePart = test.device.name.capitalize() 287 | val apkPart = dashToCamelCase(test.apk.name).capitalize() 288 | val taskName = "$runTestsTaskRobo$devicePart$apkPart" 289 | project.task(taskName, closureOf { 290 | inputs.files(resolveUnderTestApk(extension, test.apk, blankApk)) 291 | group = Constants.FIREBASE_TEST_LAB 292 | description = "Run Robo test for ${test.device.name} device on $variantName/${test.apk.name} in Firebase Test Lab" 293 | if (downloader != null) { 294 | mustRunAfter(cleanTask) 295 | } 296 | dependsOn(taskSetup) 297 | dependsOn(arrayOf(resolveAssemble(extension.testVariant))) 298 | doLast { 299 | val result = FirebaseTestLabProcessCreator.callFirebaseTestLab(ProcessData( 300 | sdk = sdk, 301 | gCloudBucketName = cloudBucketName, 302 | gCloudDirectory = cloudDirectoryName, 303 | device = test.device, 304 | apk = resolveUnderTestApk(extension, test.apk, blankApk), 305 | 306 | testType = TestType.Robo 307 | )) 308 | processResult(result, ignoreFailures) 309 | } 310 | }) 311 | } 312 | } 313 | 314 | val testResultFile = File(project.buildDir, "TestResults.txt") 315 | 316 | val instrumentationTasks: List = combineAll(appVersions, extension.testVariant.outputs) 317 | { deviceAndMap, testApk -> Test(deviceAndMap.device, deviceAndMap.apk, testApk) } 318 | .map { test -> 319 | val devicePart = test.device.name.capitalize() 320 | val apkPart = dashToCamelCase(test.apk.name).capitalize() 321 | val testApkPart = test.testApk.let { if (it.filters.isEmpty()) "" else dashToCamelCase(it.name).capitalize() } 322 | val taskName = "$runTestsTaskInstrumentation$devicePart$apkPart$testApkPart" 323 | val numShards = test.device.numShards 324 | 325 | val apkUnderTest = resolveUnderTestApk(extension, test.apk, blankApk) 326 | val testApk = resolveApk(extension.testVariant, test.testApk) 327 | val processData = ProcessData( 328 | sdk = sdk, 329 | gCloudBucketName = cloudBucketName, 330 | gCloudDirectory = cloudDirectoryName, 331 | device = test.device, 332 | apk = apkUnderTest, 333 | testType = TestType.Instrumentation(testApk) 334 | ) 335 | 336 | if (numShards > 0) { 337 | project.tasks.create(taskName, InstrumentationShardingTask::class.java) { 338 | group = Constants.FIREBASE_TEST_LAB 339 | description = "Run Instrumentation test for ${test.device.name} device on $variantName/${test.apk.name} in Firebase Test Lab" 340 | this.processData = processData 341 | this.stateFile = testResultFile 342 | 343 | if (downloader != null) { 344 | mustRunAfter(cleanTask) 345 | } 346 | dependsOn(taskSetup) 347 | dependsOn(arrayOf(resolveAssemble(extension.testVariant), resolveTestAssemble(extension.testVariant))) 348 | 349 | doFirst { 350 | testResultFile.writeText("") 351 | } 352 | 353 | doLast { 354 | val testResults = testResultFile.readText() 355 | 356 | logger.lifecycle("TESTS RESULTS: Every digit represents single shard.") 357 | logger.lifecycle("\"0\" means -> tests for particular shard passed.") 358 | logger.lifecycle("\"1\" means -> tests for particular shard failed.") 359 | 360 | logger.lifecycle("RESULTS_CODE: $testResults") 361 | logger.lifecycle("When result code is equal to 0 means that all tests for all shards passed, otherwise some of them failed.") 362 | 363 | 364 | processResult(testResults, ignoreFailures) 365 | 366 | } 367 | } 368 | 369 | } else { 370 | project.task(taskName, closureOf { 371 | inputs.files(testApk, apkUnderTest) 372 | group = Constants.FIREBASE_TEST_LAB 373 | description = "Run Instrumentation test for ${test.device.name} device on $variantName/${test.apk.name} in Firebase Test Lab" 374 | if (downloader != null) { 375 | mustRunAfter(cleanTask) 376 | } 377 | dependsOn(taskSetup) 378 | dependsOn(arrayOf(resolveAssemble(extension.testVariant), resolveTestAssemble(extension.testVariant))) 379 | doLast { 380 | logger.log(LogLevel.INFO, "Run instrumentation tests for ${this.name}") 381 | logger.log(LogLevel.DEBUG, "ProcessData for test: $processData") 382 | val result = FirebaseTestLabProcessCreator.callFirebaseTestLab(processData) 383 | logger.log(LogLevel.INFO, "Result of ${this.name}: $result") 384 | processResult(result, ignoreFailures) 385 | } 386 | }) 387 | } 388 | } 389 | 390 | val allInstrumentation: Task = project.task(runTestsTaskInstrumentation, closureOf { 391 | group = Constants.FIREBASE_TEST_LAB 392 | description = "Run all Instrumentation tests for $variantName in Firebase Test Lab" 393 | dependsOn(instrumentationTasks) 394 | 395 | doFirst { 396 | if (devices.isEmpty()) throw IllegalStateException("You need to set et least one device in:\n" + 397 | "firebaseTestLab {" + 398 | " devices {\n" + 399 | " nexus6 {\n" + 400 | " androidApiLevels = [21]\n" + 401 | " deviceIds = [\"Nexus6\"]\n" + 402 | " locales = [\"en\"]\n" + 403 | " }\n" + 404 | " } " + 405 | "}") 406 | 407 | if (instrumentationTasks.isEmpty()) throw IllegalStateException("Nothing match your filter") 408 | } 409 | }) 410 | 411 | val allRobo: Task = project.task(runTestsTaskRobo, closureOf { 412 | group = Constants.FIREBASE_TEST_LAB 413 | description = "Run all Robo tests for $variantName in Firebase Test Lab" 414 | dependsOn(roboTasks) 415 | 416 | doFirst { 417 | if (devices.isEmpty()) throw IllegalStateException("You need to set et least one device in:\n" + 418 | "firebaseTestLab {" + 419 | " devices {\n" + 420 | " nexus6 {\n" + 421 | " androidApiLevels = [21]\n" + 422 | " deviceIds = [\"Nexus6\"]\n" + 423 | " locales = [\"en\"]\n" + 424 | " }\n" + 425 | " } " + 426 | "}") 427 | 428 | if (roboTasks.isEmpty()) throw IllegalStateException("Nothing match your filter") 429 | } 430 | }) 431 | 432 | project.task(runTestsTask, closureOf { 433 | group = Constants.FIREBASE_TEST_LAB 434 | description = "Run all tests for $variantName in Firebase Test Lab" 435 | dependsOn(allRobo, allInstrumentation) 436 | }) 437 | 438 | if (downloader != null) { 439 | listOf(variantSuffix, "${variantSuffix}Instrumentation").map{suffix -> 440 | project.task(taskPrefixDownload + suffix, closureOf { 441 | group = Constants.FIREBASE_TEST_LAB 442 | description = "Run Android Tests in Firebase Test Lab and download artifacts from google storage" 443 | dependsOn(taskSetup) 444 | dependsOn(taskPrefixExecute + suffix) 445 | mustRunAfter(cleanTask) 446 | 447 | doLast { 448 | downloader.getResults() 449 | } 450 | }) 451 | } 452 | } 453 | } 454 | 455 | private fun processResult(result: TestResults, ignoreFailures: Boolean) { 456 | if (result.isSuccessful) { 457 | project.logger.lifecycle(result.message) 458 | } else { 459 | if (ignoreFailures) { 460 | project.logger.error("Error: ${result.message}") 461 | } else { 462 | throw GradleException(result.message) 463 | } 464 | } 465 | } 466 | 467 | private fun processResult(results: String, ignoreFailures: Boolean) = 468 | if (!results.contains("1")) { 469 | project.logger.lifecycle("SUCCESS: All tests passed.") 470 | } else { 471 | if (ignoreFailures) { 472 | println("FAILURE: Tests failed.") 473 | project.logger.error("FAILURE: Tests failed.") 474 | } else { 475 | throw GradleException("FAILURE: Tests failed.") 476 | } 477 | } 478 | 479 | /** 480 | * This file is used for library testing, normally, for apps, two APK files are provided, one with app and second 481 | * with tests. In case of library modules we have AAR (library archive) and APK with tests but Test Lab is not 482 | * accepting AAR as valid input (only APK). In fact library module APK file contains library module code and 483 | * testing code so single APK is sufficient to do all required tests but Firebase is still requiring APK to test, 484 | * so this is this missing APK required by Test Lab, even if it's not related to library module Firebase will 485 | * accept it and instrumentation tests will ignore it. 486 | */ 487 | private fun createBlankApkForLibrary(project: Project): File { 488 | if (!project.buildDir.exists()) { 489 | if (!project.buildDir.mkdirs()) { 490 | throw IllegalStateException("Unable to create build dir ${project.buildDir}") 491 | } 492 | } 493 | 494 | val blankApk = File(project.buildDir, "blank.apk") 495 | 496 | if (!blankApk.exists()) { 497 | try { 498 | BufferedInputStream(BLANK_APK_RESOURCE.openStream()).use { inputStream -> 499 | FileOutputStream(blankApk).use { fileOutputStream -> 500 | val data = ByteArray(1024) 501 | var byteContent = 0 502 | while ({ byteContent = inputStream.read(data, 0, 1024); byteContent }() != -1) { 503 | fileOutputStream.write(data, 0, byteContent) 504 | } 505 | } 506 | } 507 | } catch (e: IOException) { 508 | throw IllegalStateException("Unable to extract Blank APK file", e) 509 | } 510 | } 511 | return blankApk 512 | } 513 | } 514 | 515 | private fun resolveTestAssemble(variant: TestVariant): Task = try { 516 | variant.assembleProvider.get() 517 | } catch (e: IllegalStateException) { 518 | variant.assemble 519 | } 520 | 521 | private fun resolveAssemble(variant: TestVariant): Task = try { 522 | variant.testedVariant.assembleProvider.get() 523 | } catch (e: IllegalStateException) { 524 | variant.testedVariant.assemble 525 | } 526 | 527 | private fun resolveApk(variant: ApkVariant, baseVariantOutput: BaseVariantOutput): File = 528 | try { 529 | val applicationProvider = variant.packageApplicationProvider.get() 530 | applicationProvider.let { 531 | val filename = if (baseVariantOutput is ApkVariantOutput) { 532 | baseVariantOutput.outputFileName 533 | } else { 534 | it.apkNames.toList()[0] 535 | } 536 | File(it.outputDirectory.get().asFile, filename) 537 | } 538 | } catch (e: Exception) { 539 | when (e) { 540 | is IllegalStateException, is IndexOutOfBoundsException -> baseVariantOutput.outputFile 541 | else -> throw e 542 | } 543 | } 544 | 545 | private fun resolveUnderTestApk(extension: FirebaseTestLabPlugin.ExtensionType, baseVariantOutput: BaseVariantOutput, blankApk: File): File = 546 | when (extension){ 547 | is FirebaseTestLabPlugin.ExtensionType.Library -> blankApk 548 | is FirebaseTestLabPlugin.ExtensionType.Application -> resolveApk(extension.appVariant, baseVariantOutput) 549 | } 550 | 551 | private fun combineAll(l1: Collection, l2: Collection, func: (T1, T2) -> R): List = 552 | l1.flatMap { t1 -> l2.map { t2 -> func(t1, t2)} } 553 | 554 | fun dashToCamelCase(dash: String): String = 555 | dash.split('-', '_').joinToString("") { it.capitalize() } 556 | -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/FirebaseTestLabPluginExtension.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin 2 | 3 | import com.appunite.firebasetestlabplugin.model.Device 4 | import com.appunite.firebasetestlabplugin.model.ResultTypes 5 | import groovy.lang.Closure 6 | import org.gradle.api.Action 7 | import org.gradle.api.Project 8 | import java.io.File 9 | 10 | open class FirebaseTestLabPluginExtension(private val project: Project) { 11 | 12 | var keyFile: File? = null 13 | var googleProjectId: String? = null 14 | var cloudSdkPath: String? = null 15 | 16 | var cloudBucketName: String? = null 17 | var cloudDirectoryName: String? = null 18 | 19 | var clearDirectoryBeforeRun = false 20 | var ignoreFailures: Boolean = false 21 | 22 | internal val devices = project.container(Device::class.java) 23 | val resultsTypes: ResultTypes = ResultTypes() 24 | 25 | fun createDevice(name: String, action: Device.() -> Unit): Device = devices.create(name, action) 26 | fun devices(closure: Closure) { 27 | devices.configure(closure) 28 | } 29 | 30 | fun resultTypes(configure: ResultTypes.() -> Unit) { 31 | configure(resultsTypes) 32 | } 33 | 34 | fun resultTypes(closure: Closure) { 35 | project.configure(resultsTypes, closure) 36 | } 37 | } -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/cloud/CloudTestResultDownloader.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.cloud 2 | 3 | import com.appunite.firebasetestlabplugin.FirebaseTestLabPlugin 4 | import com.appunite.firebasetestlabplugin.model.ResultTypes 5 | import com.appunite.firebasetestlabplugin.utils.asCommand 6 | import org.gradle.api.GradleException 7 | import org.gradle.api.logging.Logger 8 | import java.io.File 9 | 10 | 11 | internal class CloudTestResultDownloader( 12 | private val sdk: FirebaseTestLabPlugin.Sdk, 13 | private val resultsTypes: ResultTypes, 14 | private val gCloudDirectory: File, 15 | private val resultsPath: File, 16 | private val gCloudBucketName: String, 17 | private val logger: Logger) { 18 | 19 | fun getResults() { 20 | if (!resultsTypes.junit && !resultsTypes.logcat && !resultsTypes.video && !resultsTypes.xml) { 21 | return 22 | } 23 | val gCloudFullPath = "$gCloudBucketName/$gCloudDirectory" 24 | logger.lifecycle("DOWNLOAD: Downloading results from $gCloudFullPath") 25 | 26 | prepareDownloadDirectory() 27 | downloadTestResults() 28 | } 29 | 30 | private fun prepareDownloadDirectory() { 31 | resultsPath.mkdirs() 32 | if (!resultsPath.exists()) { 33 | throw GradleException("Issue when creating destination dir $gCloudDirectory") 34 | } 35 | } 36 | 37 | private fun downloadTestResults() { 38 | val excludeQuery = StringBuilder().append("-x \".*\\.txt$|.*\\.apk$") 39 | if (!resultsTypes.xml) { 40 | excludeQuery.append("|.*\\.xml$") 41 | } 42 | if (!resultsTypes.xml) { 43 | excludeQuery.append("|.*\\.results$") 44 | } 45 | if (!resultsTypes.logcat) { 46 | excludeQuery.append("|.*\\logcat$") 47 | } 48 | if (!resultsTypes.video) { 49 | excludeQuery.append("|.*\\.mp4$") 50 | } 51 | excludeQuery.append("|.*\\.txt$\"").toString() 52 | val processCreator = ProcessBuilder("""${sdk.gsutil.absolutePath} -m rsync $excludeQuery -r gs://$gCloudBucketName/$gCloudDirectory $resultsPath""".asCommand()) 53 | val process = processCreator.start() 54 | 55 | process.errorStream.bufferedReader().forEachLine { logger.lifecycle(it) } 56 | process.inputStream.bufferedReader().forEachLine { logger.lifecycle(it) } 57 | 58 | process.waitFor() 59 | } 60 | 61 | fun clearResultsDir() { 62 | val processCreator = ProcessBuilder("""${sdk.gsutil.absolutePath} rm gs://$gCloudBucketName/$gCloudDirectory/**""".asCommand()) 63 | val process = processCreator.start() 64 | 65 | process.errorStream.bufferedReader().forEachLine { logger.lifecycle(it) } 66 | process.inputStream.bufferedReader().forEachLine { logger.lifecycle(it) } 67 | 68 | process.waitFor() 69 | } 70 | } -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/cloud/FirebaseTestLabProcessCreator.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.cloud 2 | 3 | import com.appunite.firebasetestlabplugin.FirebaseTestLabPlugin 4 | import com.appunite.firebasetestlabplugin.model.Device 5 | import com.appunite.firebasetestlabplugin.model.TestResults 6 | import com.appunite.firebasetestlabplugin.utils.joinArgs 7 | import java.io.File 8 | import java.io.Serializable 9 | 10 | sealed class TestType : Serializable { 11 | object Robo : TestType() 12 | data class Instrumentation(val testApk: File) : TestType() 13 | } 14 | 15 | data class ProcessData( 16 | val sdk: FirebaseTestLabPlugin.Sdk, 17 | val gCloudBucketName: String?, 18 | val gCloudDirectory: String?, 19 | val device: Device, 20 | val apk: File, 21 | val testType: TestType, 22 | val shardIndex: Int = -1 23 | ) : Serializable 24 | 25 | object FirebaseTestLabProcessCreator { 26 | private var execute: (ProcessData) -> Process = { processData -> createProcess(processData).start() } 27 | private val resultMessageMap = mapOf( 28 | 0 to "All test executions passed.", 29 | 1 to "A general failure occurred. Possible causes include: a filename that does not exist, or an HTTP/network error.", 30 | 2 to "Testing exited because unknown commands or arguments were provided.", 31 | 10 to "One or more test cases (tested classes or class methods) within a test execution did not pass.", 32 | 15 to "Firebase Test Lab for Android could not determine if the test matrix passed or failed because of an unexpected error.", 33 | 18 to "The test environment for this test execution is not supported because of incompatible test dimensions. This error might occur if the selected Android API level is not supported by the selected device type.", 34 | 19 to "The test matrix was canceled by the user.", 35 | 20 to "A test infrastructure error occurred." 36 | ) 37 | 38 | fun setExecutor(executor: (ProcessData) -> Process) { 39 | execute = executor 40 | } 41 | 42 | fun callFirebaseTestLab(processData: ProcessData): TestResults { 43 | val process: Process = execute(processData) 44 | val resultCode: Int = process.let { 45 | it.errorStream.bufferedReader().forEachLine { errorInfo -> println(errorInfo) } 46 | it.inputStream.bufferedReader().forEachLine { info -> println(info) } 47 | process.waitFor() 48 | } 49 | return TestResults( 50 | isSuccessful = resultCode == 0, 51 | message = resultMessageMap.getOrElse(resultCode) { "Unknown error with code: $resultCode" } 52 | ) 53 | } 54 | 55 | private fun createProcess(processData: ProcessData): ProcessBuilder { 56 | val device: Device = processData.device 57 | return ProcessBuilder( 58 | sequenceOf( 59 | processData.sdk.gcloud.absolutePath, 60 | "firebase", "test", "android", "run", 61 | "--format=json", 62 | "--device-ids=${device.deviceIds.joinArgs()}", 63 | "--app=${processData.apk}", 64 | "--locales=${device.locales.joinArgs()}", 65 | "--os-version-ids=${device.androidApiLevels.joinArgs()}", 66 | "--orientations=${device.screenOrientations.map { orientation -> orientation.gcloudName }.joinArgs()}") 67 | .plus(when (processData.testType) { 68 | TestType.Robo -> sequenceOf("--type=robo") 69 | is TestType.Instrumentation -> sequenceOf("--type=instrumentation", "--test=${processData.testType.testApk}") 70 | }) 71 | .plus(processData.gCloudBucketName?.let { sequenceOf("--results-bucket=$it") } ?: sequenceOf()) 72 | .plus(processData.gCloudDirectory?.let { sequenceOf("--results-dir=$it") } ?: sequenceOf()) 73 | .plus(if (device.isUseOrchestrator) sequenceOf("--use-orchestrator") else sequenceOf()) 74 | .plus(setupEnvironmentVariables(device, processData.shardIndex)) 75 | .plus(if (device.testTargets.isNotEmpty()) sequenceOf("--test-targets=${device.testTargets.joinToString(",")}") else sequenceOf()) 76 | .plus(device.customParamsForGCloudTool) 77 | .plus(device.testRunnerClass?.let { sequenceOf("--test-runner-class=$it") } ?: sequenceOf()) 78 | .plus(if (device.timeout > 0) sequenceOf("--timeout=${device.timeout}s") else sequenceOf()) 79 | .toList() 80 | ) 81 | } 82 | 83 | private fun setupEnvironmentVariables(device: Device, shardIndex: Int): Sequence = 84 | if (device.environmentVariables.isNotEmpty() || device.numShards > 0) 85 | sequenceOf(StringBuilder() 86 | .append("--environment-variables=") 87 | .append(if (device.environmentVariables.isNotEmpty()) device.environmentVariables.joinToString(",") else "") 88 | .append(if (device.environmentVariables.isNotEmpty() && device.numShards > 0) "," else "") 89 | .append(if (device.numShards > 0) "numShards=${device.numShards},shardIndex=$shardIndex" else "") 90 | .toString()) 91 | else sequenceOf() 92 | } 93 | -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/model/Device.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.model 2 | 3 | import java.io.Serializable 4 | 5 | class Device(val name: String) : Serializable { 6 | var locales: List = listOf("en") 7 | var screenOrientations: List = listOf(ScreenOrientation.PORTRAIT) 8 | var androidApiLevels: List = listOf() 9 | var deviceIds: List = listOf() 10 | var timeout: Long = 0 11 | 12 | var testUniversalApk: Boolean = true 13 | 14 | var filterAbiSplits = false 15 | var abiSplits: Set = setOf() 16 | var isUseOrchestrator = false 17 | var numShards = 0 18 | 19 | var environmentVariables: List = listOf() 20 | var customParamsForGCloudTool: List = listOf() 21 | var testTargets: List = listOf() 22 | var testRunnerClass: String? = null 23 | } 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/model/ResultTypes.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.model 2 | 3 | data class ResultTypes(var junit: Boolean = false, var logcat: Boolean = false, var video: Boolean = false, var xml: Boolean = false) 4 | 5 | -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/model/ScreenOrientation.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.model 2 | 3 | enum class ScreenOrientation { 4 | PORTRAIT { 5 | override val gcloudName: String = "portrait" 6 | }, 7 | LANDSCAPE { 8 | override val gcloudName: String = "landscape" 9 | }; 10 | 11 | internal abstract val gcloudName: String 12 | } 13 | -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/model/TestResults.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.model 2 | 3 | data class TestResults(val isSuccessful: Boolean, val message: String) -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/tasks/FirebaseTestLabProcess.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.tasks 2 | 3 | import com.appunite.firebasetestlabplugin.cloud.FirebaseTestLabProcessCreator 4 | import com.appunite.firebasetestlabplugin.cloud.ProcessData 5 | import org.gradle.api.GradleException 6 | import java.io.File 7 | import javax.inject.Inject 8 | 9 | class FirebaseTestLabProcess @Inject constructor( 10 | private val processData: ProcessData, 11 | private val stateFile: File 12 | ) : Runnable { 13 | override fun run() { 14 | try { 15 | val testResults = FirebaseTestLabProcessCreator.callFirebaseTestLab(processData) 16 | stateFile.appendText(text = if (testResults.isSuccessful) "0" else "1") 17 | } catch (e: Exception){ 18 | throw GradleException("There was a problem with processing ${e.message}") 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/tasks/InstrumentationShardingTask.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.tasks 2 | 3 | import com.appunite.firebasetestlabplugin.cloud.ProcessData 4 | import org.gradle.api.Action 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.OutputFile 8 | import org.gradle.api.tasks.TaskAction 9 | import org.gradle.workers.WorkerConfiguration 10 | import org.gradle.workers.WorkerExecutor 11 | import java.io.File 12 | import javax.inject.Inject 13 | 14 | open class InstrumentationShardingTask @Inject constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() { 15 | 16 | @Input 17 | lateinit var processData: ProcessData 18 | @OutputFile 19 | lateinit var stateFile: File 20 | 21 | @TaskAction 22 | fun runAction() { 23 | (0 until processData.device.numShards).map { shardIndex -> 24 | workerExecutor.submit(FirebaseTestLabProcess::class.java, object : Action { 25 | override fun execute(config: WorkerConfiguration) { 26 | config.params(processData.copy(shardIndex = shardIndex), stateFile) 27 | } 28 | }) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/utils/CloudUtils.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.utils 2 | 3 | internal fun List.joinArgs() = joinToString(",") 4 | 5 | internal fun String.asCommand(): List = split(" ", "\n").filterNot(String::isNullOrBlank) -------------------------------------------------------------------------------- /plugin/src/main/java/com/appunite/firebasetestlabplugin/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin.utils 2 | 3 | class Constants { 4 | 5 | companion object { 6 | const val GCLOUD = "gcloud" 7 | const val GSUTIL = "gsutil" 8 | const val FIREBASE_TEST_LAB = "Firebase Test Lab" 9 | } 10 | } -------------------------------------------------------------------------------- /plugin/src/main/resources/blank.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/plugin/src/main/resources/blank.apk -------------------------------------------------------------------------------- /plugin/src/test/java/com/appunite/firebasetestlabplugin/ApplicationIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin 2 | 3 | import com.android.build.gradle.AppExtension 4 | import com.appunite.firebasetestlabplugin.cloud.FirebaseTestLabProcessCreator 5 | import junit.framework.TestCase.assertTrue 6 | import org.gradle.api.Project 7 | import org.gradle.api.Task 8 | import org.gradle.api.internal.project.ProjectInternal 9 | import org.gradle.kotlin.dsl.configure 10 | import org.gradle.testfixtures.ProjectBuilder 11 | import org.junit.Test 12 | import java.io.File 13 | 14 | class ApplicationIntegrationTest { 15 | 16 | fun prepareSimpleProject(): Project { 17 | val simpleProject = File(javaClass.getResource("simple").file) 18 | val project = ProjectBuilder.builder().withProjectDir(simpleProject).build() 19 | project.plugins.apply("com.android.application") 20 | 21 | project.configure { 22 | compileSdkVersion(27) 23 | defaultConfig.apply { 24 | versionCode = 1 25 | versionName = "0.1" 26 | setMinSdkVersion(27) 27 | setTargetSdkVersion(27) 28 | } 29 | } 30 | return project 31 | } 32 | 33 | @Test 34 | fun `test evaluate simple project success`() { 35 | val project = prepareSimpleProject() 36 | (project as ProjectInternal).evaluate() 37 | } 38 | 39 | @Test 40 | fun `test evaluate simple project with plugin success`() { 41 | val simpleProject = File(javaClass.getResource("simple").file) 42 | val project = ProjectBuilder.builder().withProjectDir(simpleProject).build() 43 | project.plugins.apply("com.android.application") 44 | project.plugins.apply("firebase.test.lab") 45 | project.configure { 46 | compileSdkVersion(27) 47 | } 48 | (project as ProjectInternal).evaluate() 49 | 50 | assertTrue(project.getTasksByName("firebaseTestLabEnsureGCloudSdk", false).isNotEmpty()) 51 | assertTrue(project.getTasksByName("firebaseTestLabSetProject", false).isNotEmpty()) 52 | assertTrue(project.getTasksByName("firebaseTestLabAuth", false).isNotEmpty()) 53 | assertTrue(project.getTasksByName("firebaseTestLabSetup", false).isNotEmpty()) 54 | } 55 | 56 | @Test 57 | fun `run firebaseTestLabSetup install gcloud`() { 58 | val simpleProject = File(javaClass.getResource("simple").file) 59 | val project = prepareSimpleProject() 60 | project.plugins.apply("firebase.test.lab") 61 | project.configure { 62 | googleProjectId = "test" 63 | keyFile = File(simpleProject, "key.json") 64 | } 65 | (project as ProjectInternal).evaluate() 66 | executeTask(project, "firebaseTestLabSetProject") 67 | 68 | assertTrue(File(File(File(File(project.buildDir, "gcloud"), "google-cloud-sdk"), "bin"), "gcloud").exists()) 69 | } 70 | 71 | @Test 72 | fun `ensure after evaluation tasks presented`() { 73 | val simpleProject = File(javaClass.getResource("simple").file) 74 | val project = prepareSimpleProject() 75 | project.plugins.apply("firebase.test.lab") 76 | project.configure { 77 | googleProjectId = "test" 78 | keyFile = File(simpleProject, "key.json") 79 | createDevice("myDevice") { 80 | deviceIds = listOf("Nexus6") 81 | } 82 | } 83 | (project as ProjectInternal).evaluate() 84 | 85 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentation", false).isNotEmpty()) 86 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugRobo", false).isNotEmpty()) 87 | } 88 | 89 | @Test 90 | fun `ensure after evaluation download tasks presented`() { 91 | val simpleProject = File(javaClass.getResource("simple").file) 92 | val project = prepareSimpleProject() 93 | project.plugins.apply("firebase.test.lab") 94 | project.configure { 95 | googleProjectId = "test" 96 | cloudBucketName = "test-bucket" 97 | cloudDirectoryName = "test-directory" 98 | keyFile = File(simpleProject, "key.json") 99 | createDevice("myDevice") { 100 | deviceIds = listOf("Nexus6") 101 | } 102 | } 103 | (project as ProjectInternal).evaluate() 104 | 105 | assertTrue(project.getTasksByName("firebaseTestLabDownloadDebugInstrumentation", false).isNotEmpty()) 106 | assertTrue(project.getTasksByName("firebaseTestLabDownloadDebug", false).isNotEmpty()) 107 | } 108 | 109 | @Test 110 | fun `ensure tasks are created for abi splits with universal apk`() { 111 | val simpleProject = File(javaClass.getResource("simple").file) 112 | val project = prepareSimpleProject() 113 | project.plugins.apply("firebase.test.lab") 114 | project.configure { 115 | splits.also { 116 | it.abi.also { 117 | it.isEnable = true 118 | it.reset() 119 | it.include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") 120 | it.isUniversalApk = true 121 | } 122 | } 123 | } 124 | project.configure { 125 | googleProjectId = "test" 126 | keyFile = File(simpleProject, "key.json") 127 | createDevice("myDevice") { 128 | deviceIds = listOf("Nexus6") 129 | } 130 | } 131 | (project as ProjectInternal).evaluate() 132 | 133 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceArm64V8aDebug", false).isNotEmpty()) 134 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX86Debug", false).isNotEmpty()) 135 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX8664Debug", false).isNotEmpty()) 136 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceArmeabiV7aDebug", false).isNotEmpty()) 137 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceUniversalDebug", false).isNotEmpty()) 138 | val x86 = project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX86Debug", false).first(); 139 | FirebaseTestLabProcessCreator.setExecutor({ processData -> 140 | assertTrue("Unexpected app name ${processData.apk}", 141 | processData.apk.toString().contains("test-x86-debug.apk")) 142 | ProcessBuilder("whoami").start() 143 | }) 144 | x86.actions.forEach { it.execute(x86) } 145 | } 146 | 147 | @Test 148 | fun `ensure tasks are created for abi splits without unversal apk`() { 149 | val simpleProject = File(javaClass.getResource("simple").file) 150 | val project = prepareSimpleProject() 151 | project.plugins.apply("firebase.test.lab") 152 | project.configure { 153 | splits.also { 154 | it.abi.also { 155 | it.isEnable = true 156 | it.reset() 157 | it.include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") 158 | it.isUniversalApk = false 159 | } 160 | } 161 | } 162 | project.configure { 163 | googleProjectId = "test" 164 | keyFile = File(simpleProject, "key.json") 165 | createDevice("myDevice") { 166 | deviceIds = listOf("Nexus6") 167 | } 168 | } 169 | (project as ProjectInternal).evaluate() 170 | 171 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceArm64V8aDebug", false).isNotEmpty()) 172 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX86Debug", false).isNotEmpty()) 173 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX8664Debug", false).isNotEmpty()) 174 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceArmeabiV7aDebug", false).isNotEmpty()) 175 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceUniversalDebug", false).isEmpty()) 176 | } 177 | 178 | @Test 179 | fun `ensure tasks are created for abi splits with filter`() { 180 | val simpleProject = File(javaClass.getResource("simple").file) 181 | val project = prepareSimpleProject() 182 | project.plugins.apply("firebase.test.lab") 183 | project.configure { 184 | splits.also { 185 | it.abi.also { 186 | it.isEnable = true 187 | it.reset() 188 | it.include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") 189 | it.isUniversalApk = true 190 | } 191 | } 192 | } 193 | project.configure { 194 | googleProjectId = "test" 195 | keyFile = File(simpleProject, "key.json") 196 | createDevice("myDevice") { 197 | deviceIds = listOf("Nexus6") 198 | filterAbiSplits = true 199 | abiSplits = setOf("armeabi-v7a") 200 | testUniversalApk = false 201 | } 202 | } 203 | (project as ProjectInternal).evaluate() 204 | 205 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceArm64V8aDebug", false).isEmpty()) 206 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX86Debug", false).isEmpty()) 207 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX8664Debug", false).isEmpty()) 208 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceArmeabiV7aDebug", false).isNotEmpty()) 209 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceUniversalDebug", false).isEmpty()) 210 | } 211 | 212 | @Test 213 | fun `ensure tasks are created for abi splits with filter all`() { 214 | val simpleProject = File(javaClass.getResource("simple").file) 215 | val project = prepareSimpleProject() 216 | project.plugins.apply("firebase.test.lab") 217 | project.configure { 218 | splits.also { 219 | it.abi.also { 220 | it.isEnable = true 221 | it.reset() 222 | it.include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") 223 | it.isUniversalApk = true 224 | } 225 | } 226 | } 227 | project.configure { 228 | googleProjectId = "test" 229 | keyFile = File(simpleProject, "key.json") 230 | createDevice("myDevice") { 231 | deviceIds = listOf("Nexus6") 232 | filterAbiSplits = true 233 | } 234 | } 235 | (project as ProjectInternal).evaluate() 236 | 237 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceArm64V8aDebug", false).isEmpty()) 238 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX86Debug", false).isEmpty()) 239 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceX8664Debug", false).isEmpty()) 240 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceArmeabiV7aDebug", false).isEmpty()) 241 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceUniversalDebug", false).isNotEmpty()) 242 | } 243 | 244 | @Test 245 | fun `ensure tasks are created when abi split is disabled`() { 246 | val simpleProject = File(javaClass.getResource("simple").file) 247 | val project = prepareSimpleProject() 248 | project.plugins.apply("firebase.test.lab") 249 | 250 | project.configure { 251 | googleProjectId = "test" 252 | keyFile = File(simpleProject, "key.json") 253 | createDevice("myDevice") { 254 | deviceIds = listOf("Nexus6") 255 | } 256 | } 257 | (project as ProjectInternal).evaluate() 258 | 259 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceDebug", false).isNotEmpty()) 260 | } 261 | 262 | private fun executeTask(project: Project, taskName: String) { 263 | executeTask(project.getTasksByName(taskName, false).first()) 264 | } 265 | 266 | private fun executeTask(task: Task) { 267 | task.taskDependencies.getDependencies(task).forEach { 268 | subTask -> executeTask(subTask) 269 | } 270 | println("Executing task: ${task.name}") 271 | task.actions.forEach { it.execute(task) } 272 | } 273 | 274 | @Test 275 | fun `ensure after evaluation without shard number instrumented tasks are present`() { 276 | val simpleProject = File(javaClass.getResource("simple").file) 277 | val project = ProjectBuilder.builder().withProjectDir(simpleProject).build() 278 | project.plugins.apply("com.android.application") 279 | project.plugins.apply("firebase.test.lab") 280 | project.configure { 281 | compileSdkVersion(27) 282 | defaultConfig.also { 283 | it.versionCode = 1 284 | it.versionName = "0.1" 285 | it.setMinSdkVersion(27) 286 | it.setTargetSdkVersion(27) 287 | } 288 | } 289 | project.configure { 290 | googleProjectId = "test" 291 | keyFile = File(simpleProject, "key.json") 292 | createDevice("myDevice") { 293 | deviceIds = listOf("Nexus6") 294 | } 295 | } 296 | (project as ProjectInternal).evaluate() 297 | 298 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceDebug", false).isNotEmpty()) 299 | } 300 | 301 | @Test 302 | fun `ensure after evaluation with shard number instrumented tasks are present`() { 303 | val simpleProject = File(javaClass.getResource("simple").file) 304 | val project = ProjectBuilder.builder().withProjectDir(simpleProject).build() 305 | project.plugins.apply("com.android.application") 306 | project.plugins.apply("firebase.test.lab") 307 | project.configure { 308 | compileSdkVersion(27) 309 | defaultConfig.also { 310 | it.versionCode = 1 311 | it.versionName = "0.1" 312 | it.setMinSdkVersion(27) 313 | it.setTargetSdkVersion(27) 314 | } 315 | } 316 | project.configure { 317 | googleProjectId = "test" 318 | keyFile = File(simpleProject, "key.json") 319 | createDevice("myDevice") { 320 | deviceIds = listOf("Nexus6") 321 | numShards = 4 322 | } 323 | } 324 | (project as ProjectInternal).evaluate() 325 | 326 | assertTrue(project.getTasksByName("firebaseTestLabExecuteDebugInstrumentationMyDeviceDebug", false).isNotEmpty()) 327 | } 328 | 329 | @Test 330 | fun `ensure tasks are created correctly for flavors`() { 331 | val simpleProject = File(javaClass.getResource("simple").file) 332 | val project = prepareSimpleProject() 333 | project.plugins.apply("firebase.test.lab") 334 | project.configure { 335 | flavorDimensions("auth", "logs") 336 | productFlavors.apply { 337 | create("google").also { it.dimension = "auth" } 338 | create("facebook").also { it.dimension = "auth" } 339 | create("splunk").also { it.dimension = "logs" } 340 | create("firebase").also { it.dimension = "logs" } 341 | } 342 | } 343 | project.configure { 344 | googleProjectId = "test" 345 | keyFile = File(simpleProject, "key.json") 346 | createDevice("myDevice") { 347 | deviceIds = listOf("Nexus6") 348 | } 349 | } 350 | (project as ProjectInternal).evaluate() 351 | 352 | assertTrue(project.getTasksByName("firebaseTestLabExecuteFacebookSplunkDebugInstrumentationMyDeviceFacebookSplunkDebug", false).isNotEmpty()) 353 | assertTrue(project.getTasksByName("firebaseTestLabExecuteFacebookFirebaseDebugInstrumentationMyDeviceFacebookFirebaseDebug", false).isNotEmpty()) 354 | assertTrue(project.getTasksByName("firebaseTestLabExecuteGoogleSplunkDebugInstrumentationMyDeviceGoogleSplunkDebug", false).isNotEmpty()) 355 | assertTrue(project.getTasksByName("firebaseTestLabExecuteGoogleFirebaseDebugInstrumentationMyDeviceGoogleFirebaseDebug", false).isNotEmpty()) 356 | val facebookSplunk = project.getTasksByName("firebaseTestLabExecuteFacebookFirebaseDebugInstrumentationMyDeviceFacebookFirebaseDebug", false).first(); 357 | FirebaseTestLabProcessCreator.setExecutor { processData -> 358 | assertTrue("Unexpected app name ${processData.apk}", 359 | processData.apk.toString().contains("/facebookFirebase/debug/test-facebook-firebase-debug.apk")) 360 | ProcessBuilder("whoami").start() 361 | } 362 | facebookSplunk.actions.forEach { it.execute(facebookSplunk) } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /plugin/src/test/java/com/appunite/firebasetestlabplugin/LibraryIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebasetestlabplugin 2 | 3 | import com.android.build.gradle.LibraryExtension 4 | import org.gradle.api.Project 5 | import org.gradle.api.internal.project.ProjectInternal 6 | import org.gradle.kotlin.dsl.configure 7 | import org.gradle.testfixtures.ProjectBuilder 8 | import org.junit.Assert.assertTrue 9 | import org.junit.Test 10 | import java.io.File 11 | 12 | class LibraryIntegrationTest { 13 | 14 | fun prepareSimpleProject(): Project { 15 | val simpleProject = File(javaClass.getResource("simple").file) 16 | val project = ProjectBuilder.builder().withProjectDir(simpleProject).build() 17 | project.plugins.apply("com.android.library") 18 | project.configure { 19 | compileSdkVersion(27) 20 | defaultConfig.apply { 21 | versionCode = 1 22 | versionName = "0.1" 23 | setMinSdkVersion(27) 24 | setTargetSdkVersion(27) 25 | } 26 | } 27 | project.plugins.apply("firebase.test.lab") 28 | project.configure { 29 | googleProjectId = "test" 30 | keyFile = File(simpleProject, "key.json") 31 | createDevice("myDevice") { 32 | deviceIds = listOf("Nexus6") 33 | } 34 | } 35 | 36 | return project 37 | } 38 | 39 | @Test 40 | fun `check basic tasks`() { 41 | val project = prepareSimpleProject() 42 | (project as ProjectInternal).evaluate() 43 | 44 | assertTrue(project.getTasksByName("firebaseTestLabEnsureGCloudSdk", false).isNotEmpty()) 45 | assertTrue(project.getTasksByName("firebaseTestLabSetProject", false).isNotEmpty()) 46 | assertTrue(project.getTasksByName("firebaseTestLabAuth", false).isNotEmpty()) 47 | assertTrue(project.getTasksByName("firebaseTestLabSetup", false).isNotEmpty()) 48 | } 49 | } -------------------------------------------------------------------------------- /plugin/src/test/resources/com/appunite/firebasetestlabplugin/simple/key.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "wrong", 4 | "private_key_id": "wrong", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\nwrong\n-----END PRIVATE KEY-----\n", 6 | "client_email": "wrong@.iam.gserviceaccount.com", 7 | "client_id": "wrong", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://accounts.google.com/o/oauth2/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/wrong" 12 | } 13 | -------------------------------------------------------------------------------- /plugin/src/test/resources/com/appunite/firebasetestlabplugin/simple/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | #IntelliJ IDEA 2 | /.idea/** 3 | *.iml 4 | 5 | # Gradle 6 | /.gradle/** 7 | /.android/** 8 | /local.properties 9 | gradle.properties 10 | /build 11 | /captures 12 | /projectFilesBackup 13 | 14 | -------------------------------------------------------------------------------- /sample/app/.gitignore: -------------------------------------------------------------------------------- 1 | #IntelliJ IDEA 2 | /.idea/** 3 | *.iml 4 | key.json 5 | 6 | # Gradle 7 | /.gradle/** 8 | /.android/** 9 | /local.properties 10 | gradle.properties 11 | /build 12 | /captures 13 | /projectFilesBackup 14 | 15 | -------------------------------------------------------------------------------- /sample/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | // apply firebase test lab plugin (after `apply plugin: 'com.android.application'` ) 5 | apply plugin: 'firebase.test.lab' 6 | 7 | android { 8 | 9 | compileSdkVersion 33 10 | 11 | defaultConfig { 12 | applicationId "com.appunite.firebaselab_plugin" 13 | minSdkVersion 20 14 | targetSdkVersion 33 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | // Setup firebase test lab plugin 28 | firebaseTestLab { 29 | // REQUIRED obtain service key as described inside README 30 | keyFile = file("key.json") 31 | googleProjectId = "project-name" 32 | 33 | // If you want you can ignore test failures 34 | // ignoreFailures = true 35 | 36 | // If you prefer you can use your custom google storage bucket for storing build sources and results 37 | // cloudBucketName = "your-custome-google-storage-bucket-name" 38 | // cloudDirectoryName = "your-custome-directory-name" 39 | 40 | // If you prefer to install gcloud tool manually you can set path by 41 | // cloudSdkPath = "/user/cloud-sdk/bin" 42 | 43 | // If you want to change default gcloud installation path (default is in build/gcloud directory) 44 | // you can set environment variable `export CLOUDSDK_INSTALL_DIR=`/cache/your_directory/` 45 | 46 | // REQUIRED 47 | devices { 48 | // REQUIRED add at least one device 49 | nexusEmulator { 50 | // REQUIRED Choose at least one device id 51 | // you can list all available via `gcloud firebase test android models list` 52 | deviceIds = ["hammerhead"] 53 | 54 | // REQUIRED Choose at least one API level 55 | // you can list all available via `gcloud firebase test android models list` for your device model 56 | androidApiLevels = [23] 57 | 58 | // You can test app in landscape and portrait 59 | // screenOrientations = [com.appunite.firebasetestlabplugin.model.ScreenOrientation.PORTRAIT, com.appunite.firebasetestlabplugin.model.ScreenOrientation.LANDSCAPE] 60 | 61 | // Choose language (default is `en`) 62 | // you can list all available via `gcloud firebase test android locales list` 63 | // locales = ["en"] 64 | 65 | // If you are using ABI splits you can filter selected abi 66 | // filterAbiSplits = true 67 | // abiSplits = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"] 68 | 69 | // If you are using ABI splits you can remove testing universal APK 70 | // testUniversalApk = false 71 | 72 | // You can set timeout (in seconds) for test 73 | // timeout = 6000 74 | } 75 | // You can define more devices 76 | someOtherDevices { 77 | deviceIds = ["shamu"] 78 | androidApiLevels = [21] 79 | } 80 | } 81 | } 82 | 83 | // To see available build tasks run 84 | // ./gradlew :app:tasks 85 | // Choose from `Firebase Test Lab tasks`, like: ./gradlew :app:firebaseTestLabExecuteDebugInstrumentationNexusEmulatorDebug 86 | 87 | dependencies { 88 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 89 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 90 | testImplementation 'junit:junit:4.12' 91 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 92 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 93 | } 94 | -------------------------------------------------------------------------------- /sample/app/src/androidTest/java/com/appunite/firebaselabplugin/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebaselabplugin 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.appunite.firebaselab_plugin", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/app/src/main/java/com/appunite/firebaselabplugin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebaselabplugin 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | 6 | class MainActivity : AppCompatActivity() { 7 | 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | setContentView(R.layout.activity_main) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /sample/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /sample/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /sample/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FirebaseLab-Plugin 3 | 4 | -------------------------------------------------------------------------------- /sample/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sample/app/src/test/java/com/appunite/firebaselabplugin/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebaselabplugin 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.6.21' 5 | repositories { 6 | google() 7 | mavenCentral() 8 | 9 | maven { url 'https://jitpack.io' } 10 | maven { url "https://plugins.gradle.org/m2/" } 11 | // mavenLocal() 12 | } 13 | dependencies { 14 | classpath 'com.android.tools.build:gradle:7.2.2' 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | classpath 'firebase.test.lab:plugin:2.6.4' 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | mavenCentral() 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrmadry/FirebaseTestLab-Android/8ac8b23a2ca80d15d639a390f44398e07a727647/sample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 06 12:13:21 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 7 | -------------------------------------------------------------------------------- /sample/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /sample/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /sample/library/.gitignore: -------------------------------------------------------------------------------- 1 | #IntelliJ IDEA 2 | /.idea/** 3 | *.iml 4 | key.json 5 | 6 | # Gradle 7 | /.gradle/** 8 | /.android/** 9 | /local.properties 10 | gradle.properties 11 | /build 12 | /captures 13 | /projectFilesBackup 14 | 15 | -------------------------------------------------------------------------------- /sample/library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | // apply firebase test lab plugin (after `apply plugin: 'com.android.application'` ) 5 | apply plugin: 'firebase.test.lab' 6 | 7 | android { 8 | compileSdkVersion 30 9 | defaultConfig { 10 | minSdkVersion 20 11 | targetSdkVersion 30 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | // Setup firebase test lab plugin 25 | firebaseTestLab { 26 | 27 | keyFile = file("key.json") 28 | googleProjectId = "project-name" 29 | 30 | devices { 31 | nexusEmulator { 32 | deviceIds = ["hammerhead"] 33 | androidApiLevels = [23] 34 | } 35 | } 36 | } 37 | 38 | // To see available build tasks run 39 | // ./gradlew :app:tasks 40 | // Choose from `Firebase Test Lab tasks`, like: ./gradlew :app:firebaseTestLabExecuteDebugInstrumentationNexusEmulatorDebug 41 | 42 | dependencies { 43 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 44 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 45 | testImplementation 'junit:junit:4.12' 46 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 47 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 48 | } 49 | -------------------------------------------------------------------------------- /sample/library/src/androidTest/java/com/appunite/firebaselabplugin/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebaselabplugin 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.appunite.firebaselab_plugin", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample/library/src/main/java/com/appunite/firebaselabplugin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebaselabplugin 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | 6 | class MainActivity : AppCompatActivity() { 7 | 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/library/src/test/java/com/appunite/firebaselabplugin/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.appunite.firebaselabplugin 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /sample/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':plugin', ':sample' 2 | --------------------------------------------------------------------------------