From ec983cca15a7141eb5d301ceca8e9de329745810 Mon Sep 17 00:00:00 2001 From: joseph-cliqz Date: Thu, 11 Jul 2019 13:38:07 +0200 Subject: [PATCH] WIP --- app/build.gradle | 2 + buildSrc/build.gradle | 1 + .../com/cliqz/gradle/TestDroidTask.groovy | 116 +++++++++++ .../com/cliqz/gradle/TestdroidTask.groovy | 18 -- .../main/java/com/cliqz/gradle/CliqzPlugin.kt | 7 +- testdroid/build.gradle | 25 --- testdroid/src/main/groovy/TestDroid.groovy | 144 -------------- .../groovy/com/cliqz/testdroid/Client.groovy | 184 ------------------ 8 files changed, 125 insertions(+), 372 deletions(-) create mode 100644 buildSrc/src/main/groovy/com/cliqz/gradle/TestDroidTask.groovy delete mode 100644 buildSrc/src/main/groovy/com/cliqz/gradle/TestdroidTask.groovy delete mode 100644 testdroid/build.gradle delete mode 100644 testdroid/src/main/groovy/TestDroid.groovy delete mode 100644 testdroid/src/main/groovy/com/cliqz/testdroid/Client.groovy diff --git a/app/build.gradle b/app/build.gradle index dadce072d..a5d4936bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,7 @@ android { buildConfigField 'int', "VISIBLE_TOP_SITES", '4' applicationId "com.cliqz.browser" + testApplicationId "com.cliqz.browser.test" testInstrumentationRunner "com.cliqz.browser.test.CustomTestRunner" vectorDrawables.useSupportLibrary = true } @@ -37,6 +38,7 @@ android { shrinkResources false proguardFiles 'proguard-project.txt' versionNameSuffix '-debug' + dexcount.enabled = false } release { diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 89a1b1d02..f21eac1bf 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -10,6 +10,7 @@ repositories { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation "com.android.tools.build:gradle:3.4.1" + implementation "com.squareup.okhttp3:okhttp:4.0.0" } // Clearing dependencies order, we do not want to compile java at all diff --git a/buildSrc/src/main/groovy/com/cliqz/gradle/TestDroidTask.groovy b/buildSrc/src/main/groovy/com/cliqz/gradle/TestDroidTask.groovy new file mode 100644 index 000000000..2849e1b5c --- /dev/null +++ b/buildSrc/src/main/groovy/com/cliqz/gradle/TestDroidTask.groovy @@ -0,0 +1,116 @@ +package com.cliqz.gradle + +import okhttp3.FormBody +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import okio.BufferedSink +import org.apache.http.entity.mime.content.FileBody +import org.bouncycastle.cert.ocsp.Req +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import groovy.json.JsonSlurper +import org.jetbrains.annotations.NotNull + +class TestDroidTask extends DefaultTask { + + static final PROJECT_ID = '207029628' + static final API_KEY = 'WnxRgICLtoIRfDqlPDYcM4xr0xTLFv7H' + static final FRAMEWORK_ID = '252' + static final DEVICE_GROUP_ID = '40437' + + final OkHttpClient client = new OkHttpClient() + final slurper = new JsonSlurper() + + @TaskAction + def connectTestdroid() { + // 1. Does the project identified by the id param exists? Decide yourself how to pass this param + final url = new URL("https://cloud.bitbar.com/api/me/projects/${PROJECT_ID}") + final request = url.openConnection() + request.setRequestProperty('Authorization', "Basic ${getBasicAuth(API_KEY)}") + if (request.responseCode == HttpURLConnection.HTTP_OK) { + logger.info("Project exists") + } else { + logger.error("No project for the given id: ${PROJECT_ID}") + } + // 2. Upload the 2 apks and store the ids + final apkID = uploadFile("/Users/kiizajosephbazaare/browser-android/app/build/outputs/apk/cliqz/debug/app-cliqz-arm64-v8a-debug.apk") + final testsID = uploadFile("/Users/kiizajosephbazaare/browser-android/app/build/outputs/apk/androidTest/cliqz/debug/app-cliqz-debug-androidTest.apk") + // 3. Create the testrun and run it + final runID = performTestRun(apkID, testsID) + // 4. Poll every minute (os so) to check the job finished + String state = pollTestRunStatus(runID) + int timeTaken = 0 + while (state!= "FINISHED"){ + sleep(60000) + timeTaken+=1 + state = pollTestRunStatus(runID) + } + logger.info("Tests finished in ${timeTaken} minutes") + // 5. Download the artifacts and put them in a predefined folder (where Azure/Jenkins can find it) + // 6. log error if any test failed, otherwise just return + } + + def getBasicAuth(String apiKey) { + "${apiKey}:".bytes.encodeBase64().toString() + } + + def uploadFile(String filepath){ + final file = new File(filepath) + final mediaType = MediaType.parse('application/octet-stream') + def requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart('file', file.name, RequestBody.create(file, mediaType)) + .build() + def request = new Request.Builder() + .header('Authorization', "Basic ${getBasicAuth(API_KEY)}") + .url("https://cloud.bitbar.com/api/me/files") + .post(requestBody) + .build() + final response = client.newCall(request).execute() + if (!response.isSuccessful()) throw new IOException("Unexpected file" + response.code) + final json = slurper.parseText(response.body.string()) + json.id + } + + def performTestRun(long appFileID, long testFileID){ + final mediaType = MediaType.parse('application/json') + final content = """ + { + "osType":"ANDROID", + "projectId":"$PROJECT_ID", + "files":[ + {"id":"$appFileID"}, + {"id":"$testFileID"} + ], + "frameworkId":"$FRAMEWORK_ID", + "deviceGroupId":"$DEVICE_GROUP_ID" + }""".stripIndent() + + def request = new Request.Builder() + .header('Authorization', "Basic ${getBasicAuth(API_KEY)}") + .url("https://cloud.bitbar.com/api/me/runs") + .post(RequestBody.create(content, mediaType)) + .build() + final response = client.newCall(request).execute() + if (!response.isSuccessful()) throw new IOException("Test run not possible " + response.code) + final testRun = slurper.parseText(response.body.string()) + response.close() + testRun.id + } + + def pollTestRunStatus(long testID){ + final poll = new URL("https://cloud.bitbar.com/api/me/projects/${PROJECT_ID}/runs/${testID}") + final check = poll.openConnection() + check.setRequestProperty('Authorization', "Basic ${getBasicAuth(API_KEY)}") + if (check.responseCode == HttpURLConnection.HTTP_OK){ + final pollStatus = slurper.parseText (check.content.text) + pollStatus.state + } else { + logger.error("No Test Run for the given id: ${testID}") + } + } +} diff --git a/buildSrc/src/main/groovy/com/cliqz/gradle/TestdroidTask.groovy b/buildSrc/src/main/groovy/com/cliqz/gradle/TestdroidTask.groovy deleted file mode 100644 index f9e05eee3..000000000 --- a/buildSrc/src/main/groovy/com/cliqz/gradle/TestdroidTask.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package com.cliqz.gradle - -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.TaskAction - -class TestdroidTask extends DefaultTask { - - @TaskAction - def connectTestdroid() { - println("Hello, world!!!") - // 1. Does the project identified by the id param exists? Decide yourself how to pass this param - // 2. Upload the 2 apks and store the ids - // 3. Create the testrun and run it - // 4. Poll every minute (os so) to check the job finished - // 5. Download the artifacts and put them in a predefined folder (where Azure/Jenkins can find it) - // 6. log error if any test failed, otherwise just return - } -} diff --git a/buildSrc/src/main/java/com/cliqz/gradle/CliqzPlugin.kt b/buildSrc/src/main/java/com/cliqz/gradle/CliqzPlugin.kt index d5dbdceb7..d4162afae 100644 --- a/buildSrc/src/main/java/com/cliqz/gradle/CliqzPlugin.kt +++ b/buildSrc/src/main/java/com/cliqz/gradle/CliqzPlugin.kt @@ -35,7 +35,12 @@ class CliqzPlugin: Plugin { private fun createTestdroidTask(project: Project, variant: ApplicationVariant) { val taskName = "connect${variant.name.capitalize()}TestDroid" // Make sure this task depends on the assembling of the app and tests, we need those file to be uploaded - project.tasks.register(taskName, TestdroidTask::class.java) + val assembleTestProvider = project.tasks.named("assemble${variant.name.capitalize()}AndroidTest") + val assembleProvider = project.tasks.named("assemble${variant.name.capitalize()}") + val taskProvider = project.tasks.register(taskName, TestDroidTask::class.java) + taskProvider.configure { + it.dependsOn(assembleProvider, assembleTestProvider) + } } private fun createCliqzConfigTasks(project: Project, variant: ApplicationVariant) { diff --git a/testdroid/build.gradle b/testdroid/build.gradle deleted file mode 100644 index 64ec8e9a9..000000000 --- a/testdroid/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -apply plugin: 'groovy' - -targetCompatibility = '1.7' -sourceCompatibility = '1.7' - -//create a single Jar with all dependencies -task('testdroid-cli', type: Jar) { - manifest { - attributes 'Implementation-Title': 'Gradle Jar File Example', - 'Implementation-Version': '1.0', - 'Main-Class': 'TestDroid' - } - baseName = project.name + '-cli' - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } - with jar -} - -dependencies { - compile 'com.testdroid:testdroid-api:2.39' - compile 'org.codehaus.groovy:groovy-all:2.4.7' - compile 'commons-cli:commons-cli:1.2' - compile("jline:jline:2.11") { - exclude(group: 'junit', module: 'junit') - } -} diff --git a/testdroid/src/main/groovy/TestDroid.groovy b/testdroid/src/main/groovy/TestDroid.groovy deleted file mode 100644 index f490be7ce..000000000 --- a/testdroid/src/main/groovy/TestDroid.groovy +++ /dev/null @@ -1,144 +0,0 @@ -import com.cliqz.testdroid.Client - -import static com.cliqz.testdroid.Client.client - -/** - * @author Stefano Pacifici - */ -class TestDroid { - def cli = new CliBuilder(usage: 'testdroid [options]', header: 'Options:') - - void errorMessage(msg) { - println msg - cli.usage() - System.exit(1) - } - - static void main(String... args) { - def testdroid = new TestDroid() - System.exit(testdroid.run(args)) - } - - static def parseTestType(str) { - switch (str) { - case 'xc': - return Client.xc - default: - return Client.android - } - - } - - // Return a value that you can use as Unix error code (0 success, any other value failure) - def run(String... args) { - cli._(longOpt: 'env', 'All parameters are passed trough the environment') - cli.u(longOpt: 'user', args: 1, argName: 'user name', 'Testdroid username') - cli._(longOpt: 'help', 'Print this help message') - cli._(longOpt: 'app', args: 1, argName: 'app path', 'The path of the apk_ipa to test') - cli._(longOpt: 'tests', args: 1, argName: 'tests path', 'Tha path of the automation tests apk_zip') - cli.p(longOpt: 'project', args: 1, argName: 'project name', 'Testdroid project name') - cli.r(longOpt: 'run', args: 1, argName: 'run name', 'Testdroid run name') - cli.d(longOpt: 'dev', args: 1, argName: 'device group', 'Testdroid devices group to use') - cli._(longOpt: 'dst', args: 1, argName: 'dest folder', 'Test artifacts destination folder') - cli.t(longOpt: 'type', args: 1, argName: 'test type', 'The test type (default: android, values: android, xc)') - - def options = cli.parse(args) - - def user - def pass = null - def devGroup - def projectName - def testRunName - def appPackage - def testsPackage - def dstFolder - def type - - if (options.help) { - cli.usage() - return - } - - if (options.env) { - def env = System.getenv() - user = env['TESTDROID_USER'] - pass = env['TESTDROID_PASS'] - devGroup = env['TESTDROID_DEVICE_GROUP'] - projectName = env['TESTDROID_PROJECT_NAME'] - testRunName = env['TESTDROID_RUN_NAME'] - appPackage = env['APK_IPA'] - testsPackage = env['TEST_APK_ZIP'] - dstFolder = env['ARTIFACTS_FOLDER'] - type = parseTestType(env['TESTDROID_TEST_TYPE']) - } else { - user = options.u - devGroup = options.d - projectName = options.p - testRunName = options.r - appPackage = options.app - testsPackage = options.tests - dstFolder = options.dst - type = parseTestType(options.t) - } - - // Check parameters - if (!user) { - errorMessage 'User name is mandatory' - } - if (!projectName) { - errorMessage 'Project name is required' - } - if (!devGroup) { - devGroup = 14 - } - if (!checkFile(appPackage)) { - errorMessage "App file not found ${appPackage}" - } - if (!checkFile(testsPackage)) { - errorMessage "Tests file not found ${testsPackage}" - } - if (!testRunName) { - testRunName = "Run on ${new Date().format("yyyy/MM/dd HH:mm")}" - } - if (!options.env) { - // Try to read the password from the shell - print "Password: " - def passwordChars = System.console().readPassword() - pass = new String(passwordChars) - } else if (!pass) { - errorMessage 'Password not present in the environment' - } - - client { - username user - password pass - // apiKey API_KEY // apiKey(API_KEY) - project projectName - testRun testRunName - deviceGroup devGroup - testType type - appFile appPackage - testFile testsPackage - artifactsFolder file(dstFolder) - } - } - - static boolean checkFile(path) { - try { - new File(path).isFile() - } catch (ignored) { - false - } - } - - static boolean checkFolder(path) { - try { - new File(path).isDirectory() - } catch (ignored) { - false - } - } - - static File file(path) { new File(path) } -} - diff --git a/testdroid/src/main/groovy/com/cliqz/testdroid/Client.groovy b/testdroid/src/main/groovy/com/cliqz/testdroid/Client.groovy deleted file mode 100644 index a6a8a05c8..000000000 --- a/testdroid/src/main/groovy/com/cliqz/testdroid/Client.groovy +++ /dev/null @@ -1,184 +0,0 @@ -package com.cliqz.testdroid - -import com.testdroid.api.APIFilter.APIFilterItem -import com.testdroid.api.APIList -import com.testdroid.api.APIQueryBuilder -import com.testdroid.api.DefaultAPIClient -import com.testdroid.api.model.APIDeviceSession -import com.testdroid.api.model.APIProject -import com.testdroid.api.model.APITestRun -import com.testdroid.api.model.APIUser - -class Client { - - final static SUCCESS_ERROR_CODE = 0 - final static NO_TEST_ERROR_CODE = 1 - final static TOO_MANY_TEST_FAILURES_ERROR_CODE = 2 - - final static float SUCCESS_RATIO_THRESHOLD = 0.95 - - final ANDROID_FILE_MIME_TYPE = "application/vnd.android.package-archive" - final API_URL = 'https://cloud.testdroid.com' - private final params = [:] - - static def android = APIProject.Type.ANDROID - static def xc = APIProject.Type.XCTEST - - Client() { - params['deviceGroup'] = 14 - params['testType'] = android - } - - def methodMissing(String name, args) { - params.put(name, args[0]) - } - - def propertyMissing(String name) { - final value = params[name] - if (!value) { - throw new MissingPropertyException(name, this.class) - } - value - } - - static def log(msg, closure) { - print "${msg}... " - def result = closure() - println 'DONE' - result - } - - @SuppressWarnings("GrReassignedInClosureLocalVar") - def getProject(me) { - APIProject project - log("Check project if project ${params.project} exists") { - APIQueryBuilder builder = new APIQueryBuilder(); - APIFilterItem item = new APIFilterItem(APIFilterItem.Type.S, - 'name', APIFilterItem.Operand.EQ, params.project) - builder.filter([item]) - project = me.getProjectsResource(builder).entity.find { APIProject pr -> - pr.name == params.project - } - } - if (project == null) { - project = log("Creating project ${params.project}") { - me.createProject(params.testType, params.project) - } - } - project - } - - // Project id: 109335866 - // Test run: 109336945 - def execute() { - final client = new DefaultAPIClient(API_URL, params['username'], params['password']) - final APIUser me = log('Logging in') { client.me() } - // A project is typically a branch - final APIProject project = getProject(me) - log('Updating configuration') { - final config = project.testRunConfig - config.instrumentationRunner = 'com.cliqz.browser.test.CustomTestRunner' - config.timeout = 600000 - if (params.deviceGroup) { - try { - config.usedDeviceGroupId = params.deviceGroup as Long - config.usedDeviceGroupName = "" - } catch (ignored) { - config.usedDeviceGroupName = params.deviceGroup - config.usedDeviceGroupId = 0 - } - } - config.update() - } - def mimetype - if (params.testType == android) { - mimetype = ANDROID_FILE_MIME_TYPE - } else { - mimetype = 'application/octet-stream' - } - - log('Uploading APK_IPA') { - project.uploadApplication(new File(params.appFile), mimetype) - } - log('Uploading test APK_ZIP') { - project.uploadTest(new File(params.testFile), mimetype) - } - print 'Running tests' - final testRun = params.testRun ? - project.run(params.testRun) : project.run() - - // Wait for test completion - def abortTime = System.currentTimeMillis() + 1800000L; - while (testRun.state != APITestRun.State.FINISHED) { - sleep(10000) - print '.' - testRun.update() - def now = System.currentTimeMillis(); - if (testRun.runningDeviceCount == 0 && abortTime - now < 0) { - testRun.abort() - break; - } - if (testRun.runningDeviceCount > 0) { - abortTime = System.currentTimeMillis() + 600000L; - } - } - println ' DONE' - if (testRun.finishedDeviceCount == 0) { - return NO_TEST_ERROR_CODE - } - testRun.deviceSessions.iterator().findAll({ session -> - session.state == APIDeviceSession.State.SUCCEEDED - }).each { session -> - final deviceName = session.device.displayName - final dst = new File(params.artifactsFolder, "${deviceName}.zip") - dst.delete() - dst.append(session.outputFiles) - } - if (testRun.successRatio < SUCCESS_RATIO_THRESHOLD) { - TOO_MANY_TEST_FAILURES_ERROR_CODE - } else { - SUCCESS_ERROR_CODE - } - } - - /** - * Execute the tests on testdroid platform using the given configuration - * - * @param closure the configuration closure - * @return 0 if success, some value > 0 in case of error - */ - static client(closure) { - final cl = new Client() - closure.delegate = cl; - closure() - cl.execute() - } - - static { - APIUser.metaClass.getProjects = { -> projectsResource.entity } - - APIProject.metaClass.getTestRuns = { -> testRunsResource.entity } - - APITestRun.metaClass.getFiles = { -> filesResource.entity } - APITestRun.metaClass.getDeviceSessions { -> deviceSessionsResource.entity } - - APIList.metaClass.iterator = { - def list = delegate as APIList - def iter = list.data.iterator() - [ - hasNext: { -> - if (iter.hasNext()) { - true - } else if (list.nextAvailable) { - list = list.nextItems - iter = list.data.iterator() - iter.hasNext() - } else { - false - } - }, - next: { -> iter.next() } - ] as Iterator - } - } -}