From ca03c476f21f8667744b44c885d3c2129d4e8165 Mon Sep 17 00:00:00 2001 From: OliverShen <41765758+olivershen-wow@users.noreply.github.com> Date: Tue, 14 Feb 2023 18:09:49 +0800 Subject: [PATCH] Add yml config usage for gradle plugin (#263) Refactor to decouple params of different scopes; Add yml config for gradle plugin; Add type param to testSpec section of yml config, remove extraArgs; --- build.gradle | 2 +- .../common/entity/center/TestTaskSpec.java | 3 + .../common/entity/common/TestTask.java | 7 +- .../UML/gradle_plugin_yaml_config_design.png | Bin 0 -> 118002 bytes gradle_plugin/README.md | 56 ++-- gradle_plugin/build.gradle | 4 +- .../UML/gradle_plugin_yaml_config_design.puml | 68 +++++ .../hydralab/ClientUtilsPlugin.groovy | 254 ++++++++---------- .../hydralab/config/DeviceConfig.java | 39 +++ .../{entity => config}/HydraLabAPIConfig.java | 40 +-- .../microsoft/hydralab/config/TestConfig.java | 90 +++++++ .../hydralab/entity/AttachmentInfo.java | 2 + .../hydralab/entity/BlobFileInfo.java | 2 + .../hydralab/entity/DeviceAction.java | 12 + .../hydralab/entity/DeviceTestResult.java | 2 + .../microsoft/hydralab/entity/TestTask.java | 2 + .../microsoft/hydralab/utils/CommonUtils.java | 30 +++ .../hydralab/utils/HydraLabAPIClient.java | 70 ++--- .../hydralab/utils/HydraLabClientUtils.java | 140 ++++------ .../microsoft/hydralab/utils/YamlParser.java | 53 ++++ .../src/main/resources/template/build.gradle | 13 - .../hydralab/ClientUtilsPluginTest.java | 212 ++++++++------- gradle_plugin/template/build.gradle | 22 ++ .../resources => }/template/gradle.properties | 15 +- gradle_plugin/template/testSpec.yml | 52 ++++ 25 files changed, 737 insertions(+), 453 deletions(-) create mode 100644 docs/images/UML/gradle_plugin_yaml_config_design.png create mode 100644 gradle_plugin/doc/UML/gradle_plugin_yaml_config_design.puml create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/DeviceConfig.java rename gradle_plugin/src/main/groovy/com/microsoft/hydralab/{entity => config}/HydraLabAPIConfig.java (60%) create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/TestConfig.java create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceAction.java create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/YamlParser.java delete mode 100644 gradle_plugin/src/main/resources/template/build.gradle create mode 100644 gradle_plugin/template/build.gradle rename gradle_plugin/{src/main/resources => }/template/gradle.properties (81%) create mode 100644 gradle_plugin/template/testSpec.yml diff --git a/build.gradle b/build.gradle index 5ca000cdd..8e36e5c87 100644 --- a/build.gradle +++ b/build.gradle @@ -93,7 +93,7 @@ import com.microsoft.hydralab.compile.UMLImageGenerator task generateUMLImage(group: 'documentation') { doFirst { - def scanningDirList = ['agent/doc/UML'] + def scanningDirList = ['agent/doc/UML', 'gradle_plugin/doc/UML'] def outputDir = new File(projectDir, 'docs/images/UML') def generator = new UMLImageGenerator() diff --git a/common/src/main/java/com/microsoft/hydralab/common/entity/center/TestTaskSpec.java b/common/src/main/java/com/microsoft/hydralab/common/entity/center/TestTaskSpec.java index 9f4a6dc0b..acb056df3 100644 --- a/common/src/main/java/com/microsoft/hydralab/common/entity/center/TestTaskSpec.java +++ b/common/src/main/java/com/microsoft/hydralab/common/entity/center/TestTaskSpec.java @@ -28,7 +28,10 @@ public class TestTaskSpec { public boolean isPerfTest; public boolean needUninstall = true; public boolean needClearData = true; + // todo: remove this field when update overall center-ADO/Gradle plugins compatibility + @Deprecated public Map instrumentationArgs; + public Map testRunArgs; public Set agentIds = new HashSet<>(); public String runningType; public int maxStepCount = 100; diff --git a/common/src/main/java/com/microsoft/hydralab/common/entity/common/TestTask.java b/common/src/main/java/com/microsoft/hydralab/common/entity/common/TestTask.java index 1e64e0847..9f3a918c8 100644 --- a/common/src/main/java/com/microsoft/hydralab/common/entity/common/TestTask.java +++ b/common/src/main/java/com/microsoft/hydralab/common/entity/common/TestTask.java @@ -110,7 +110,12 @@ public static TestTask convertToTestTask(TestTaskSpec testTaskSpec) { testTask.setTimeOutSecond(testTaskSpec.testTimeOutSec); testTask.setNeededPermissions(testTaskSpec.neededPermissions); testTask.setDeviceActions(testTaskSpec.deviceActions); - testTask.setInstrumentationArgs(testTaskSpec.instrumentationArgs); + if (testTaskSpec.instrumentationArgs != null) { + testTask.setInstrumentationArgs(testTaskSpec.instrumentationArgs); + } + else { + testTask.setInstrumentationArgs(testTaskSpec.testRunArgs); + } testTask.setFileSetId(testTaskSpec.fileSetId); testTask.setPkgName(testTaskSpec.pkgName); testTask.setTestPkgName(testTaskSpec.testPkgName); diff --git a/docs/images/UML/gradle_plugin_yaml_config_design.png b/docs/images/UML/gradle_plugin_yaml_config_design.png new file mode 100644 index 0000000000000000000000000000000000000000..2f687e59c752d58d7575467e0dcae03a7df7f92d GIT binary patch literal 118002 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 a@0Tod?4Rfiq=XRvY;EDNtHj*<=)V9>uRKct literal 0 HcmV?d00001 diff --git a/gradle_plugin/README.md b/gradle_plugin/README.md index def2fd16e..d15a84856 100644 --- a/gradle_plugin/README.md +++ b/gradle_plugin/README.md @@ -2,46 +2,28 @@ This is the Gradle plugin of Hydra Lab. In order to simplify the onboarding procedure to Hydra Lab for any app, this project packaged the client util and made it an easy way for any app to leverage the cloud testing service of Hydra Lab. -## Prerequisite -Include Hydra Lab plugin dependency in build.gradle of your project: -- Using the plugins DSL: -``` -plugins { - id "com.microsoft.hydralab.client-util" version "${plugin_version}" -} -``` -- Using legacy plugin application: -``` -buildscript { - repositories { - maven { - url "https://plugins.gradle.org/m2/" - } - } - dependencies { - classpath "com.microsoft.hydralab:gradle_plugin:${plugin_version}" - } -} - -apply plugin: "com.microsoft.hydralab.client-util" -``` -See [Release Notes](https://github.com/microsoft/HydraLab/wiki/Release-Notes) for latest and stable versions. ## Usage -To trigger gradle task for Hydra Lab testing, simply follow below steps: -- Step 1: go to [template](https://github.com/microsoft/HydraLab/tree/main/gradle_plugin/src/main/resources/template) page, copy the following files to your repo and modify the content: - - [build.gradle](https://github.com/microsoft/HydraLab/blob/main/gradle_plugin/src/main/resources/template/build.gradle) - - To introduce dependency on this plugin, please copy all content to repository/module you would like to use the plugin in. - - [gradle.properties](https://github.com/microsoft/HydraLab/blob/main/gradle_plugin/src/main/resources/template/gradle.properties) - - According to the comment inline and the running type you choose for your test, you should keep all required parameters and fill in them with correct values. -- Step 2: Build your project/module to enable the Gradle plugin and task +To trigger Hydra Lab testing using Gradle command, simply follow below steps: +- Step 1: go to [template](https://github.com/microsoft/HydraLab/tree/main/gradle_plugin/template) page, selectively leverage the following files to your repo and modify the content: + - To introduce dependency on this plugin, please copy according content in [build.gradle](https://github.com/microsoft/HydraLab/tree/main/gradle_plugin/template/build.gradle) to your project/module. + - See [release notes](https://github.com/microsoft/HydraLab/wiki/Release-Notes) for version info and version number. + - Update **${plugin_version}** with your selected version. + - According to your project structure, apply one or combination of the following configuration approaches to configure the input parameters of gradle plugin task (see detailed explanation for parameters in [gradle.properties](https://github.com/microsoft/HydraLab/tree/main/gradle_plugin/template/gradle.properties)) + - **Parameter priority: inline command > gradle.properties > yaml** + - Inline gradle command, set parameters with "-Pxxx=yyy". + - Sample: **gradle [:${MODULE_NAME}:]requestHydraLabTest -PappPath="${PATH_TO_APP}" -PtestAppPath=...** + - [gradle.properties](https://github.com/microsoft/HydraLab/tree/main/gradle_plugin/template/gradle.properties) + - Usage: + - Fill in the file, keep only the parameters needed for your test, and remove the redundant ones. + - Keep this file in the same directory as your build.gradle, gradle task will read this file automatically. + - [testSpec.yml](https://github.com/microsoft/HydraLab/tree/main/gradle_plugin/template/testSpec.yml) + - Usage: + - Fill in the file, keep only the parameters needed for your test, and remove the redundant ones. + - Specific the yml file path by inline command "-PymlConfigFile=${PATH_TO_YML}" following the gradle task command. + - Sample: **gradle [:${MODULE_NAME}:]requestHydraLabTest -PymlConfigFile=${PATH_TO_YML} ...** +- Step 2: Build your project/module to enable the gradle plugin and task - Step 3: Run gradle task requestHydraLabTest - - Use gradle command to trigger the task. - - Override any value in gradle.properties by specify command param "-PXXX=xxx". - - Example command: **gradle requestHydraLabTest -PappApkPath="D:\Test Folder\app.apk"** ## Known issue - Hard-coded with Azure DevOps embedded variable names, currently may not be compatible to other CI tools when fetching commit related information. - -## TODO -**- Add yml configuration file for task param setup.** diff --git a/gradle_plugin/build.gradle b/gradle_plugin/build.gradle index 86b59c5b4..a99fa5fd8 100644 --- a/gradle_plugin/build.gradle +++ b/gradle_plugin/build.gradle @@ -32,10 +32,12 @@ dependencies { implementation 'org.ow2.asm:asm:7.0' implementation 'org.ow2.asm:asm-util:7.0' implementation networkDependencies.okHttp + implementation 'org.yaml:snakeyaml:1.33' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.0.1' } // plugin publishing related -version = '1.0.45' +version = '1.1.0' group = 'com.microsoft.hydralab' // alter group to this when publish to local, in order to distinguish local version and gradle plugin portal version //group = 'com.microsoft.hydralab.local' diff --git a/gradle_plugin/doc/UML/gradle_plugin_yaml_config_design.puml b/gradle_plugin/doc/UML/gradle_plugin_yaml_config_design.puml new file mode 100644 index 000000000..ee1b2ba40 --- /dev/null +++ b/gradle_plugin/doc/UML/gradle_plugin_yaml_config_design.puml @@ -0,0 +1,68 @@ +@startyaml +hydraLabAPIServer: + host: example.center-endpoint.com + schema: https + authToken: xxxxxxxxxx +testSpec: + device: + deviceIdentifier: RANDOMDEVICESERIALNUMBER123 + groupTestType: SINGLE + deviceActions: + setUp: + - deviceType: Android + method: setProperty + args: + - xxx + - xxx + - deviceType: Android + method: pushFileToDevice + args: + - xxx + tearDown: + - deviceType: Android + method: setProperty + args: + - xxx + - deviceType: Android + method: pullFileFromDevice + args: + - xxx + triggerType: API + runningType: INSTRUMENTATION + appPath: ABSOLUTE_PATH_TO_APP_FILE + pkgName: app.pkg.name + testAppPath: ABSOLUTE_PATH_TO_TEST_APP_FILE + testPkgName: test_app.pkg.name + teamName: Default + testRunnerName: androidx.test.runner.AndroidJUnitRunner + testScope: CLASS + testSuiteName: test.suite.class.name + frameworkType: JUNIT4 + runTimeOutSeconds: 1000 + queueTimeOutSeconds: 500 + needUninstall: true + needClearData: true + neededPermissions: + - android.permission.READ_CONTACTS + - android.permission.WRITE_CONTACTS + attachmentConfigPath: ABSOLUTE_PATH_TO_ATTACHMENT_CONFIG_FILE + attachmentInfos: + - fileName: a.json + filePath: ABSOLUTE_PATH_TO_A_JSON + fileType: COMMON + loadType: COPY + loadDir: DIR_TO_COPY_A_TO + - fileName: b.json + filePath: ABSOLUTE_PATH_TO_A_JSON + fileType: COMMON + loadType: COPY + loadDir: DIR_TO_COPY_B_TO + artifactTag: artifact_file_name_tag + testRunArgs: + key1: value1 + key2: value2 + exploration: + maxStepCount: 100 + testRound: -1 + +@endyaml \ No newline at end of file diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/ClientUtilsPlugin.groovy b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/ClientUtilsPlugin.groovy index 5176d8068..4b06fcc4f 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/ClientUtilsPlugin.groovy +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/ClientUtilsPlugin.groovy @@ -2,8 +2,12 @@ // Licensed under the MIT License. package com.microsoft.hydralab -import com.microsoft.hydralab.entity.HydraLabAPIConfig +import com.microsoft.hydralab.config.DeviceConfig +import com.microsoft.hydralab.config.HydraLabAPIConfig +import com.microsoft.hydralab.config.TestConfig +import com.microsoft.hydralab.utils.CommonUtils import com.microsoft.hydralab.utils.HydraLabClientUtils +import com.microsoft.hydralab.utils.YamlParser import org.apache.commons.lang3.StringUtils import org.gradle.api.Plugin import org.gradle.api.Project @@ -14,96 +18,35 @@ class ClientUtilsPlugin implements Plugin { void apply(Project target) { target.task("requestHydraLabTest") { doFirst { - def runningType = "" - if (project.hasProperty('runningType')) { - runningType = project.runningType - } - def deviceIdentifier = "" - if (project.hasProperty('deviceIdentifier')) { - deviceIdentifier = project.deviceIdentifier - } - def runTimeOutSeconds = "" - if (project.hasProperty('runTimeOutSeconds')) { - runTimeOutSeconds = project.runTimeOutSeconds - } - def queueTimeOutSeconds = runTimeOutSeconds - if (project.hasProperty('queueTimeOutSeconds')) { - queueTimeOutSeconds = project.queueTimeOutSeconds + HydraLabAPIConfig apiConfig = new HydraLabAPIConfig() + TestConfig testConfig = new TestConfig() + + def reportDir = new File(project.buildDir, "testResult") + if (!reportDir.exists()) { + reportDir.mkdirs() } - def testSuiteName = "" - if (project.hasProperty('testSuiteName')) { - testSuiteName = project.testSuiteName + + // read config from yml + if (project.hasProperty('ymlConfigFile')) { + YamlParser yamlParser = new YamlParser(project.ymlConfigFile) + apiConfig = yamlParser.parseAPIConfig() + testConfig = yamlParser.parseTestConfig() } - def appPath = "" if (project.hasProperty('appPath')) { - def appFile = project.file(project.appPath) - println("Param appPath: ${project.appPath}") - if (!appFile.exists()) { - def exceptionMsg = "${project.appPath} file not exist!" - throw new Exception(exceptionMsg) - } else { - appPath = appFile.absolutePath - } + testConfig.appPath = project.appPath } - - def testAppPath = "" if (project.hasProperty('testAppPath')) { - def testAppFile = project.file(project.testAppPath) - println("Param testAppPath: ${project.testAppPath}") - if (!testAppFile.exists()) { - def exceptionMsg = "${project.testAppPath} file not exist!" - throw new Exception(exceptionMsg) - } else { - testAppPath = testAppFile.absolutePath - } + testConfig.testAppPath = project.testAppPath } - - def attachmentConfigPath = "" if (project.hasProperty('attachmentConfigPath')) { - def attachmentConfigFile = project.file(project.attachmentConfigPath) - println("Param attachmentConfigPath: ${project.attachmentConfigPath}") - if (!attachmentConfigFile.exists()) { - def exceptionMsg = "${project.attachmentConfigPath} file not exist!" - throw new Exception(exceptionMsg) - } else { - attachmentConfigPath = attachmentConfigFile.absolutePath - } - } - - def reportDir = new File(project.buildDir, "testResult") - if (!reportDir.exists()) reportDir.mkdirs() - - def argsMap = null - if (project.hasProperty('instrumentationArgs')) { - argsMap = [:] - // quotation marks not support - def argLines = project.instrumentationArgs.replace("\"", "").split(",") - for (i in 0.. { if (project.hasProperty('authToken')) { apiConfig.authToken = project.authToken } - if (project.hasProperty('onlyAuthPost')) { - apiConfig.onlyAuthPost = Boolean.parseBoolean(project.onlyAuthPost) - } - if (project.hasProperty('pkgName')) { - apiConfig.pkgName = project.pkgName + + if (testConfig.deviceConfig == null) { + testConfig.deviceConfig = new DeviceConfig() } - if (project.hasProperty('testPkgName')) { - apiConfig.testPkgName = project.testPkgName + if (project.hasProperty('deviceIdentifier')) { + testConfig.deviceConfig.deviceIdentifier = project.deviceIdentifier } if (project.hasProperty('groupTestType')) { - apiConfig.groupTestType = project.groupTestType + testConfig.deviceConfig.groupTestType = project.groupTestType } - if (project.hasProperty('frameworkType')) { - apiConfig.frameworkType = project.frameworkType + if (project.hasProperty('deviceActions')) { + // add quotes back as quotes in gradle plugins will be replaced by blanks + testConfig.deviceConfig.deviceActionsStr = project.deviceActions.replace("\\", "\"") } - if (project.hasProperty('maxStepCount')) { - apiConfig.maxStepCount = Integer.parseInt(project.maxStepCount) + + if (project.hasProperty('triggerType')) { + testConfig.triggerType = project.triggerType + } + // @Deprecated + else if (project.hasProperty('type')) { + testConfig.triggerType = project.type + } + if (project.hasProperty('runningType')) { + testConfig.runningType = project.runningType + } + if (project.hasProperty('pkgName')) { + testConfig.pkgName = project.pkgName } - if (project.hasProperty('deviceTestCount')) { - apiConfig.deviceTestCount = Integer.parseInt(project.deviceTestCount) + if (project.hasProperty('testPkgName')) { + testConfig.testPkgName = project.testPkgName } if (project.hasProperty('teamName')) { - apiConfig.teamName = project.teamName + testConfig.teamName = project.teamName } if (project.hasProperty('testRunnerName')) { - apiConfig.testRunnerName = project.testRunnerName + testConfig.testRunnerName = project.testRunnerName } if (project.hasProperty('testScope')) { - apiConfig.testScope = project.testScope + testConfig.testScope = project.testScope + } + if (project.hasProperty('testSuiteName')) { + testConfig.testSuiteName = project.testSuiteName + } + if (project.hasProperty('frameworkType')) { + testConfig.frameworkType = project.frameworkType + } + if (project.hasProperty('runTimeOutSeconds')) { + testConfig.runTimeOutSeconds = Integer.parseInt(project.runTimeOutSeconds) + } + if (project.hasProperty('queueTimeOutSeconds')) { + testConfig.queueTimeOutSeconds = Integer.parseInt(project.queueTimeOutSeconds) + } else { + if (!project.hasProperty('ymlConfigFile')) { + testConfig.queueTimeOutSeconds = testConfig.runTimeOutSeconds + } } if (project.hasProperty('needUninstall')) { - apiConfig.needUninstall = Boolean.parseBoolean(project.needUninstall) + testConfig.needUninstall = Boolean.parseBoolean(project.needUninstall) } if (project.hasProperty('needClearData')) { - apiConfig.needClearData = Boolean.parseBoolean(project.needClearData) + testConfig.needClearData = Boolean.parseBoolean(project.needClearData) } if (project.hasProperty('neededPermissions')) { - apiConfig.neededPermissions = project.neededPermissions.split(", +") + testConfig.neededPermissions = project.neededPermissions.split(", +") } - if (project.hasProperty('deviceActions')) { - // add quotes back as quotes in gradle plugins will be replaced by blanks - apiConfig.deviceActionsStr = project.deviceActions.replace("\\", "\"") + if (project.hasProperty('artifactTag')) { + testConfig.artifactTag = project.artifactTag + } + // @Deprecated + else if (project.hasProperty('tag')) { + testConfig.artifactTag = project.tag + } + if (project.hasProperty('testRunArgs')) { + testConfig.testRunArgs = CommonUtils.parseArguments(project.testRunArgs) + } + // @Deprecated + else if (project.hasProperty('instrumentationArgs')) { + testConfig.testRunArgs = CommonUtils.parseArguments(project.instrumentationArgs) + } + if (project.hasProperty('maxStepCount')) { + testConfig.maxStepCount = Integer.parseInt(project.maxStepCount) + } + if (project.hasProperty('testRound')) { + testConfig.testRound = Integer.parseInt(project.testRound) + } + // @Deprecated + else if (project.hasProperty('deviceTestCount')) { + testConfig.testRound = Integer.parseInt(project.deviceTestCount) } - requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig) + requiredParamCheck(apiConfig, testConfig) - HydraLabClientUtils.runTestOnDeviceWithApp( - runningType, appPath, testAppPath, attachmentConfigPath, - testSuiteName, deviceIdentifier, Integer.parseInt(queueTimeOutSeconds), Integer.parseInt(runTimeOutSeconds), - reportDir.absolutePath, argsMap, extraArgsMap, tag, - apiConfig - ) + HydraLabClientUtils.runTestOnDeviceWithApp(reportDir.absolutePath, apiConfig, testConfig) } }.configure { group = "Test" @@ -173,47 +157,48 @@ class ClientUtilsPlugin implements Plugin { } } - void requiredParamCheck(String runningType, String appPath, String testAppPath, String deviceIdentifier, String runTimeOutSeconds, String testSuiteName, HydraLabAPIConfig apiConfig) { - if (StringUtils.isBlank(runningType) - || StringUtils.isBlank(appPath) - || StringUtils.isBlank(apiConfig.pkgName) - || StringUtils.isBlank(deviceIdentifier) - || StringUtils.isBlank(runTimeOutSeconds) + void requiredParamCheck(HydraLabAPIConfig apiConfig, TestConfig testConfig) { + if (StringUtils.isBlank(apiConfig.host) || StringUtils.isBlank(apiConfig.authToken) + || StringUtils.isBlank(testConfig.appPath) + || StringUtils.isBlank(testConfig.pkgName) + || StringUtils.isBlank(testConfig.runningType) + || testConfig.runTimeOutSeconds == 0 + || StringUtils.isBlank(testConfig.deviceConfig.deviceIdentifier) ) { - throw new IllegalArgumentException('Required params not provided! Make sure the following params are all provided correctly: authToken, appPath, pkgName, runningType, deviceIdentifier, runTimeOutSeconds.') + throw new IllegalArgumentException('Required params not provided! Make sure the following params are all provided correctly: hydraLabAPIhost, authToken, deviceIdentifier, appPath, pkgName, runningType, runTimeOutSeconds.') } // running type specified params - switch (runningType) { + switch (testConfig.runningType) { case "INSTRUMENTATION": - if (StringUtils.isBlank(testAppPath)) { - throw new IllegalArgumentException('Required param testAppPath not provided!') + if (StringUtils.isBlank(testConfig.testAppPath)) { + throw new IllegalArgumentException('Running type ' + testConfig.runningType + ' required param testAppPath not provided!') } - if (StringUtils.isBlank(apiConfig.testPkgName)) { - throw new IllegalArgumentException('Required param testPkgName not provided!') + if (StringUtils.isBlank(testConfig.testPkgName)) { + throw new IllegalArgumentException('Running type ' + testConfig.runningType + ' required param testPkgName not provided!') } - if (apiConfig.testScope != TestScope.PACKAGE && apiConfig.testScope != TestScope.CLASS) { + if (testConfig.testScope != TestScope.PACKAGE && testConfig.testScope != TestScope.CLASS) { break } - if (StringUtils.isBlank(testSuiteName)) { - throw new IllegalArgumentException('Required param testSuiteName not provided!') + if (StringUtils.isBlank(testConfig.testSuiteName)) { + throw new IllegalArgumentException('Running type ' + testConfig.runningType + ' required param testSuiteName not provided!') } break case "APPIUM": - if (StringUtils.isBlank(testAppPath)) { - throw new IllegalArgumentException('Required param testAppPath not provided!') + if (StringUtils.isBlank(testConfig.testAppPath)) { + throw new IllegalArgumentException('Running type ' + testConfig.runningType + ' required param testAppPath not provided!') } - if (StringUtils.isBlank(testSuiteName)) { - throw new IllegalArgumentException('Required param testSuiteName not provided!') + if (StringUtils.isBlank(testConfig.testSuiteName)) { + throw new IllegalArgumentException('Running type ' + testConfig.runningType + ' required param testSuiteName not provided!') } break case "APPIUM_CROSS": - if (StringUtils.isBlank(testAppPath)) { - throw new IllegalArgumentException('Required param testAppPath not provided!') + if (StringUtils.isBlank(testConfig.testAppPath)) { + throw new IllegalArgumentException('Running type ' + testConfig.runningType + ' required param testAppPath not provided!') } - if (StringUtils.isBlank(testSuiteName)) { - throw new IllegalArgumentException('Required param testSuiteName not provided!') + if (StringUtils.isBlank(testConfig.testSuiteName)) { + throw new IllegalArgumentException('Running type ' + testConfig.runningType + ' required param testSuiteName not provided!') } break case "SMART": @@ -229,7 +214,6 @@ class ClientUtilsPlugin implements Plugin { } } - interface TestScope { String TEST_APP = "TEST_APP"; String PACKAGE = "PACKAGE"; diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/DeviceConfig.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/DeviceConfig.java new file mode 100644 index 000000000..10c14f25a --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/DeviceConfig.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package com.microsoft.hydralab.config; + +import com.microsoft.hydralab.entity.DeviceAction; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.microsoft.hydralab.utils.CommonUtils.GSON; + +/** + * @author Li Shen + * @date 2/8/2023 + */ + +public class DeviceConfig { + public String deviceIdentifier = ""; + public String groupTestType = "SINGLE"; + public Map> deviceActions = new HashMap<>(); + public String deviceActionsStr = ""; + + public void extractFromExistingField(){ + if (StringUtils.isBlank(this.deviceActionsStr) && deviceActions.size() != 0) { + this.deviceActionsStr = GSON.toJson(this.deviceActions); + } + } + + @Override + public String toString() { + return "DeviceConfig:\n" + + "\tdeviceIdentifier=" + deviceIdentifier + "\n" + + "\tgroupTestType=" + groupTestType + "\n" + + "\tdeviceActionsStr=" + deviceActionsStr; + } +} \ No newline at end of file diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/HydraLabAPIConfig.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/HydraLabAPIConfig.java similarity index 60% rename from gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/HydraLabAPIConfig.java rename to gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/HydraLabAPIConfig.java index b6f98b94e..f27188fd2 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/HydraLabAPIConfig.java +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/HydraLabAPIConfig.java @@ -1,17 +1,15 @@ -package com.microsoft.hydralab.entity; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package com.microsoft.hydralab.config; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; import java.util.Locale; -// todo: split into APIConfig/deviceConfig/testConfig public class HydraLabAPIConfig { public String schema = "https"; public String host = ""; public String contextPath = ""; public String authToken = ""; - public boolean onlyAuthPost = true; - public String checkCenterVersionAPIPath = "/api/center/info"; public String checkCenterAliveAPIPath = "/api/center/isAlive"; public String getBlobSAS = "/api/package/getSAS"; public String uploadAPKAPIPath = "/api/package/add"; @@ -22,20 +20,6 @@ public class HydraLabAPIConfig { public String cancelTestTaskAPIPath = "/api/test/task/cancel/%s?reason=%s"; public String testPortalTaskInfoPath = "/portal/index.html?redirectUrl=/info/task/"; public String testPortalTaskDeviceVideoPath = "/portal/index.html?redirectUrl=/info/videos/"; - public String pkgName = ""; - public String testPkgName = ""; - public String groupTestType = "SINGLE"; - public String pipelineLink = ""; - public String frameworkType = "JUnit4"; - public int maxStepCount = 100; - public int deviceTestCount = -1; - public boolean needUninstall = true; - public boolean needClearData = true; - public String teamName = ""; - public String testRunnerName = "androidx.test.runner.AndroidJUnitRunner"; - public String testScope = ""; - public List neededPermissions = new ArrayList<>(); - public String deviceActionsStr = ""; public String getBlobSASUrl() { return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, getBlobSAS); @@ -80,19 +64,7 @@ public String getDeviceTestVideoUrl(String id) { @Override public String toString() { return "HydraLabAPIConfig:\n" + - "pkgName=" + pkgName + ",\n" + - "testPkgName=" + testPkgName + ",\n" + - "groupTestType=" + groupTestType + ",\n" + - "pipelineLink=" + pipelineLink + ",\n" + - "frameworkType=" + frameworkType + ",\n" + - "maxStepCount=" + maxStepCount + ",\n" + - "deviceTestCount=" + deviceTestCount + ",\n" + - "needUninstall=" + needUninstall + ",\n" + - "needClearData=" + needClearData + ",\n" + - "teamName=" + teamName + ",\n" + - "testRunnerName=" + testRunnerName + ",\n" + - "testScope=" + testScope + ",\n" + - "neededPermissions=" + (neededPermissions != null ? neededPermissions.toString() : "") + ",\n" + - "deviceActionsStr=" + deviceActionsStr; + "\tschema=" + schema + "\n" + + "\thost=" + host; } } \ No newline at end of file diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/TestConfig.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/TestConfig.java new file mode 100644 index 000000000..13b0bb57f --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/config/TestConfig.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package com.microsoft.hydralab.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.hydralab.entity.AttachmentInfo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Li Shen + * @date 2/8/2023 + */ + +public class TestConfig { + public String triggerType = "API"; + @JsonProperty("device") + public DeviceConfig deviceConfig = new DeviceConfig(); + public String runningType = ""; + public String appPath = ""; + public String testAppPath = ""; + public String pkgName = ""; + public String testPkgName = ""; + public String teamName = ""; + public String testRunnerName = "androidx.test.runner.AndroidJUnitRunner"; + public String testScope = ""; + public String testSuiteName = ""; + public String frameworkType = "JUnit4"; + public int runTimeOutSeconds = 0; + public int queueTimeOutSeconds = 0; + public String pipelineLink = ""; + public boolean needUninstall = true; + public boolean needClearData = true; + public List neededPermissions = new ArrayList<>(); + // priority: config file path in param > direct yml config + public String attachmentConfigPath = ""; + public List attachmentInfos = new ArrayList<>(); + public String artifactTag = ""; + public Map testRunArgs; + public int maxStepCount = 100; + public int testRound = -1; + + public void constructField(HashMap map) { + Object queueTimeOutSeconds = map.get("queueTimeOutSeconds"); + if (queueTimeOutSeconds == null) { + this.queueTimeOutSeconds = this.runTimeOutSeconds; + } + HashMap explorationArgs = (HashMap)map.get("exploration"); + Object maxStepCount = explorationArgs.get("maxStepCount"); + if (maxStepCount != null) { + this.maxStepCount = Integer.parseInt(maxStepCount.toString()); + } + Object testRound = explorationArgs.get("testRound"); + if (testRound != null) { + this.testRound = Integer.parseInt(testRound.toString()); + } + } + + @Override + public String toString() { + return "TestConfig:\n" + + "\t" + deviceConfig.toString() + "\n" + + "\ttriggerType=" + triggerType + "\n" + + "\trunningType=" + runningType + "\n" + + "\tappPath=" + appPath + "\n" + + "\ttestAppPath=" + testAppPath + "\n" + + "\tpkgName=" + pkgName + "\n" + + "\ttestPkgName=" + testPkgName + "\n" + + "\tteamName=" + teamName + "\n" + + "\ttestRunnerName=" + testRunnerName + "\n" + + "\ttestScope=" + testScope + "\n" + + "\ttestSuiteName=" + testSuiteName + "\n" + + "\tframeworkType=" + frameworkType + "\n" + + "\trunTimeOutSeconds=" + runTimeOutSeconds + "\n" + + "\tqueueTimeOutSeconds=" + queueTimeOutSeconds + "\n" + + "\tpipelineLink=" + pipelineLink + "\n" + + "\tneedUninstall=" + needUninstall + "\n" + + "\tneedClearData=" + needClearData + "\n" + + "\tneededPermissions=" + (neededPermissions != null ? neededPermissions.toString() : "") + "\n" + + "\tattachmentConfigPath=" + attachmentConfigPath + "\n" + + "\tattachmentConfigs=" + attachmentInfos.toString() + "\n" + + "\tartifactTag=" + artifactTag + "\n" + + "\ttestRunArgs=" + testRunArgs + "\n" + + "\tmaxStepCount=" + maxStepCount + "\n" + + "\ttestRound=" + testRound; + } +} diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/AttachmentInfo.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/AttachmentInfo.java index 81685c124..cf60cf8e3 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/AttachmentInfo.java +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/AttachmentInfo.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. package com.microsoft.hydralab.entity; public class AttachmentInfo { diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/BlobFileInfo.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/BlobFileInfo.java index a7a7299ce..b844a7ae0 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/BlobFileInfo.java +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/BlobFileInfo.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. package com.microsoft.hydralab.entity; import com.google.gson.JsonObject; diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceAction.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceAction.java new file mode 100644 index 000000000..d73d46acb --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceAction.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package com.microsoft.hydralab.entity; + +import java.util.ArrayList; +import java.util.List; + +public class DeviceAction { + public String deviceType; + public String method; + public List args = new ArrayList<>(); +} \ No newline at end of file diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceTestResult.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceTestResult.java index 749cf189e..e4df0a104 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceTestResult.java +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceTestResult.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. package com.microsoft.hydralab.entity; import java.util.List; diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/TestTask.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/TestTask.java index 767e603cc..81b36040c 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/TestTask.java +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/TestTask.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. package com.microsoft.hydralab.entity; import java.util.Date; diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/CommonUtils.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/CommonUtils.java index cfe7305af..fa831f25c 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/CommonUtils.java +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/CommonUtils.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. package com.microsoft.hydralab.utils; import com.google.gson.Gson; @@ -5,12 +7,15 @@ import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import org.apache.commons.lang3.StringUtils; +import java.io.File; import java.io.IOException; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; +import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -41,6 +46,31 @@ public Date read(JsonReader in) throws IOException { } }).create(); + public static String validateAndReturnFilePath(String filePath, String paramName) throws IllegalArgumentException { + assertNotNull(filePath, paramName); + File file = new File(filePath); + assertTrue(file.exists(), filePath + " file not exist!", null); + + System.out.println("Param " + paramName + ": " + filePath + " validated."); + return file.getAbsolutePath(); + } + + public static HashMap parseArguments(String argsString){ + if (StringUtils.isBlank(argsString)) { + return null; + } + HashMap argsMap = new HashMap<>(); + + // quotation marks not support + String[] argLines = argsString.replace("\"", "").split(","); + for (String argLine: argLines) { + String[] kv = argLine.split("="); + argsMap.put(kv[0], kv[1]); + } + + return argsMap; + } + public static String maskCred(String content) { for (HydraLabClientUtils.MaskSensitiveData sensitiveData : HydraLabClientUtils.MaskSensitiveData.values()) { Pattern PATTERNCARD = Pattern.compile(sensitiveData.getRegEx(), Pattern.CASE_INSENSITIVE); diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabAPIClient.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabAPIClient.java index cf06258f3..f82dc408c 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabAPIClient.java +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabAPIClient.java @@ -1,9 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. package com.microsoft.hydralab.utils; import com.google.gson.*; -import com.microsoft.hydralab.entity.AttachmentInfo; -import com.microsoft.hydralab.entity.HydraLabAPIConfig; -import com.microsoft.hydralab.entity.TestTask; +import com.microsoft.hydralab.config.DeviceConfig; +import com.microsoft.hydralab.config.HydraLabAPIConfig; +import com.microsoft.hydralab.config.TestConfig; +import com.microsoft.hydralab.entity.*; import okhttp3.*; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -52,7 +55,7 @@ public void checkCenterAlive(HydraLabAPIConfig apiConfig) { } } - public String uploadApp(HydraLabAPIConfig apiConfig, String commitId, String commitCount, String commitMsg, File app, File testApp) { + public String uploadApp(HydraLabAPIConfig apiConfig, TestConfig testConfig, String commitId, String commitCount, String commitMsg, File app, File testApp) { checkCenterAlive(apiConfig); MediaType contentType = MediaType.get("application/vnd.android.package-archive"); @@ -62,8 +65,8 @@ public String uploadApp(HydraLabAPIConfig apiConfig, String commitId, String com .addFormDataPart("commitCount", commitCount) .addFormDataPart("commitMessage", commitMsg) .addFormDataPart("appFile", app.getName(), RequestBody.create(contentType, app)); - if (!StringUtils.isEmpty(apiConfig.teamName)) { - multipartBodyBuilder.addFormDataPart("teamName", apiConfig.teamName); + if (!StringUtils.isEmpty(testConfig.teamName)) { + multipartBodyBuilder.addFormDataPart("teamName", testConfig.teamName); } if (testApp != null) { multipartBodyBuilder.addFormDataPart("testAppFile", testApp.getName(), RequestBody.create(contentType, testApp)); @@ -149,12 +152,12 @@ public JsonObject addAttachment(HydraLabAPIConfig apiConfig, String testFileSetI } } - public String generateAccessKey(HydraLabAPIConfig apiConfig, String deviceIdentifier) { + public String generateAccessKey(HydraLabAPIConfig apiConfig, TestConfig testConfig) { checkCenterAlive(apiConfig); Request req = new Request.Builder() .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(String.format(apiConfig.getGenerateAccessKeyUrl(), deviceIdentifier)) + .url(String.format(apiConfig.getGenerateAccessKeyUrl(), testConfig.deviceConfig.deviceIdentifier)) .get() .build(); OkHttpClient clientToUse = client; @@ -189,39 +192,41 @@ public String generateAccessKey(HydraLabAPIConfig apiConfig, String deviceIdenti } } - public JsonObject triggerTestRun(String runningType, HydraLabAPIConfig apiConfig, String fileSetId, String testSuiteName, - String deviceIdentifier, @Nullable String accessKey, int runTimeoutSec, Map instrumentationArgs, Map extraArgs) { + public JsonObject triggerTestRun(TestConfig testConfig, HydraLabAPIConfig apiConfig, String fileSetId, @Nullable String accessKey) { checkCenterAlive(apiConfig); + DeviceConfig deviceConfig = testConfig.deviceConfig; + JsonObject jsonElement = new JsonObject(); - jsonElement.addProperty("runningType", runningType); - jsonElement.addProperty("deviceIdentifier", deviceIdentifier); + jsonElement.addProperty("type", testConfig.triggerType); + jsonElement.addProperty("runningType", testConfig.runningType); + jsonElement.addProperty("deviceIdentifier", deviceConfig.deviceIdentifier); jsonElement.addProperty("fileSetId", fileSetId); - jsonElement.addProperty("testSuiteClass", testSuiteName); - jsonElement.addProperty("testTimeOutSec", runTimeoutSec); - jsonElement.addProperty("pkgName", apiConfig.pkgName); - jsonElement.addProperty("testPkgName", apiConfig.testPkgName); - jsonElement.addProperty("groupTestType", apiConfig.groupTestType); - jsonElement.addProperty("pipelineLink", apiConfig.pipelineLink); - jsonElement.addProperty("frameworkType", apiConfig.frameworkType); - jsonElement.addProperty("maxStepCount", apiConfig.maxStepCount); - jsonElement.addProperty("deviceTestCount", apiConfig.deviceTestCount); - jsonElement.addProperty("needUninstall", apiConfig.needUninstall); - jsonElement.addProperty("needClearData", apiConfig.needClearData); - jsonElement.addProperty("testRunnerName", apiConfig.testRunnerName); - jsonElement.addProperty("testScope", apiConfig.testScope); + jsonElement.addProperty("testSuiteClass", testConfig.testSuiteName); + jsonElement.addProperty("testTimeOutSec", testConfig.runTimeOutSeconds); + jsonElement.addProperty("pkgName", testConfig.pkgName); + jsonElement.addProperty("testPkgName", testConfig.testPkgName); + jsonElement.addProperty("groupTestType", deviceConfig.groupTestType); + jsonElement.addProperty("pipelineLink", testConfig.pipelineLink); + jsonElement.addProperty("frameworkType", testConfig.frameworkType); + jsonElement.addProperty("maxStepCount", testConfig.maxStepCount); + jsonElement.addProperty("deviceTestCount", testConfig.testRound); + jsonElement.addProperty("needUninstall", testConfig.needUninstall); + jsonElement.addProperty("needClearData", testConfig.needClearData); + jsonElement.addProperty("testRunnerName", testConfig.testRunnerName); + jsonElement.addProperty("testScope", testConfig.testScope); try { - if (apiConfig.neededPermissions.size() > 0) { - jsonElement.add("neededPermissions", GSON.toJsonTree(apiConfig.neededPermissions)); + if (testConfig.neededPermissions.size() > 0) { + jsonElement.add("neededPermissions", GSON.toJsonTree(testConfig.neededPermissions)); } - if (StringUtils.isNotBlank(apiConfig.deviceActionsStr)) { + if (StringUtils.isNotBlank(deviceConfig.deviceActionsStr)) { JsonParser parser = new JsonParser(); - JsonObject jsonObject = parser.parse(apiConfig.deviceActionsStr).getAsJsonObject(); + JsonObject jsonObject = parser.parse(deviceConfig.deviceActionsStr).getAsJsonObject(); jsonElement.add("deviceActions", jsonObject); } - if (instrumentationArgs != null) { - jsonElement.add("instrumentationArgs", GSON.toJsonTree(instrumentationArgs).getAsJsonObject()); + if (testConfig.testRunArgs != null) { + jsonElement.add("testRunArgs", GSON.toJsonTree(testConfig.testRunArgs).getAsJsonObject()); } } catch (JsonParseException e) { @@ -231,9 +236,6 @@ public JsonObject triggerTestRun(String runningType, HydraLabAPIConfig apiConfig if (accessKey != null) { jsonElement.addProperty("accessKey", accessKey); } - if (extraArgs != null) { - extraArgs.forEach(jsonElement::addProperty); - } String content = GSON.toJson(jsonElement); printlnf("triggerTestRun api post body: %s", maskCred(content)); diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabClientUtils.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabClientUtils.java index 082863e1f..404921c01 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabClientUtils.java +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabClientUtils.java @@ -1,14 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. package com.microsoft.hydralab.utils; import com.google.gson.*; -import com.microsoft.hydralab.entity.HydraLabAPIConfig; -import com.microsoft.hydralab.entity.AttachmentInfo; -import com.microsoft.hydralab.entity.BlobFileInfo; -import com.microsoft.hydralab.entity.DeviceTestResult; -import com.microsoft.hydralab.entity.TestTask; +import com.microsoft.hydralab.config.DeviceConfig; +import com.microsoft.hydralab.config.HydraLabAPIConfig; +import com.microsoft.hydralab.config.TestConfig; +import com.microsoft.hydralab.entity.*; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.Nullable; import java.io.*; import java.nio.charset.StandardCharsets; @@ -32,51 +32,15 @@ public static void switchClientInstance(HydraLabAPIClient client) { hydraLabAPIClient = client; } - public static void runTestOnDeviceWithApp(String runningType, String appPath, String testAppPath, - String attachmentConfigPath, - String testSuiteName, - @Nullable String deviceIdentifier, - int queueTimeoutSec, - int runTimeoutSec, - String reportFolderPath, - Map instrumentationArgs, - Map extraArgs, - String tag, - HydraLabAPIConfig apiConfig) { - String output = String.format("##[section]All args: runningType: %s, appPath: %s, deviceIdentifier: %s" + - "\n##[section]\tqueueTimeOutSeconds: %d, runTimeOutSeconds: %d, argsMap: %s, extraArgsMap: %s" + - "\n##[section]\tapiConfig: %s", - runningType, appPath, deviceIdentifier, - queueTimeoutSec, runTimeoutSec, instrumentationArgs == null ? "" : instrumentationArgs.toString(), extraArgs == null ? "" : extraArgs.toString(), - apiConfig.toString()); - switch (runningType) { - case "INSTRUMENTATION": - case "APPIUM": - case "APPIUM_CROSS": - output = output + String.format("\n##[section]\ttestApkPath: %s, testSuiteName: %s", testAppPath, testSuiteName); - break; - case "T2C_JSON": - output = output + String.format("\n##[section]\ttestApkPath: %s", testAppPath); - break; - case "SMART": - case "MONKEY": - case "APPIUM_MONKEY": - default: - break; - } - if (StringUtils.isNotEmpty(attachmentConfigPath)) { - output = output + String.format("\n##[section]\tattachmentConfigPath: %s", attachmentConfigPath); - } - if (StringUtils.isNotEmpty(tag)) { - output = output + String.format("\n##[section]\ttag: %s", tag); - } + public static void runTestOnDeviceWithApp(String reportFolderPath, HydraLabAPIConfig apiConfig, TestConfig testConfig) { + String output = String.format("##[section]All args: reportFolderPath: %s\n%s\n%s", + reportFolderPath, apiConfig.toString(), testConfig.toString()); printlnf(maskCred(output)); isTestRunningFailed = false; try { - runTestInner(runningType, appPath, testAppPath, attachmentConfigPath, testSuiteName, deviceIdentifier, - queueTimeoutSec, runTimeoutSec, reportFolderPath, instrumentationArgs, extraArgs, tag, apiConfig); + runTestInner(reportFolderPath, apiConfig, testConfig); markRunningSuccess(); } catch (RuntimeException e) { markRunningFail(); @@ -84,17 +48,7 @@ public static void runTestOnDeviceWithApp(String runningType, String appPath, St } } - private static void runTestInner(String runningType, String appPath, String testAppPath, - String attachmentConfigPath, - String testSuiteName, - @Nullable String deviceIdentifier, - int queueTimeoutSec, - int runTimeoutSec, - String reportFolderPath, - Map instrumentationArgs, - Map extraArgs, - String tag, - HydraLabAPIConfig apiConfig) { + private static void runTestInner(String reportFolderPath, HydraLabAPIConfig apiConfig, TestConfig testConfig) { // Collect git info File commandDir = new File("."); // TODO: make the commit info fetch approach compatible to other types of pipeline variables. @@ -129,9 +83,8 @@ private static void runTestInner(String runningType, String appPath, String test File app = null; File testApp = null; - JsonArray attachmentInfos = new JsonArray(); try { - File file = new File(appPath); + File file = new File(testConfig.appPath); assertTrue(file.exists(), "app not exist", null); if (file.isDirectory()) { @@ -140,8 +93,8 @@ private static void runTestInner(String runningType, String appPath, String test app = file; } - if (!testAppPath.isEmpty()) { - file = new File(testAppPath); + if (StringUtils.isNotEmpty(testConfig.testAppPath)) { + file = new File(testConfig.testAppPath); assertTrue(file.exists(), "testApp not exist", null); if (file.isDirectory()) { throw new IllegalArgumentException("testAppPath should be the path to the test app/jar or JSON-described test file."); @@ -150,34 +103,34 @@ private static void runTestInner(String runningType, String appPath, String test } } - if (!attachmentConfigPath.isEmpty()) { - file = new File(attachmentConfigPath); + if (StringUtils.isNotBlank(testConfig.attachmentConfigPath)) { + file = new File(testConfig.attachmentConfigPath); JsonParser parser = new JsonParser(); - attachmentInfos = parser.parse(new FileReader(file)).getAsJsonArray(); - printlnf("Attachment size: %d", attachmentInfos.size()); - printlnf("Attachment information: %s", attachmentInfos.toString()); + JsonArray attachmentInfoJsons = parser.parse(new FileReader(file)).getAsJsonArray(); + printlnf("Attachment size: %d", attachmentInfoJsons.size()); + printlnf("Attachment information: %s", attachmentInfoJsons.toString()); + + // new a list to override yml config if the file path exists + testConfig.attachmentInfos = new ArrayList<>(); + for (JsonElement attachmentInfoJson : attachmentInfoJsons) { + AttachmentInfo attachmentInfo = GSON.fromJson(attachmentInfoJson, AttachmentInfo.class); + testConfig.attachmentInfos.add(attachmentInfo); + } } } catch (Exception e) { throw new IllegalArgumentException("Apps not found, or attachment config not extracted correctly: " + e.getMessage(), e); } - if (apiConfig == null) { - apiConfig = new HydraLabAPIConfig(); - } - - String testFileSetId = hydraLabAPIClient.uploadApp(apiConfig, commitId, commitCount, commitMsg, app, testApp); + String testFileSetId = hydraLabAPIClient.uploadApp(apiConfig, testConfig, commitId, commitCount, commitMsg, app, testApp); printlnf("##[section]Uploaded test file set id: %s", testFileSetId); assertNotNull(testFileSetId, "testFileSetId"); // TODO: make the pipeline link fetch approach compatible to other types of pipeline variables. - apiConfig.pipelineLink = System.getenv("SYSTEM_TEAMFOUNDATIONSERVERURI") + System.getenv("SYSTEM_TEAMPROJECT") + "/_build/results?buildId=" + System.getenv("BUILD_BUILDID"); - printlnf("##[section]Callback pipeline link is: %s", apiConfig.pipelineLink); - - for (int index = 0; index < attachmentInfos.size(); index++) { - JsonObject attachmentJson = attachmentInfos.get(index).getAsJsonObject(); - AttachmentInfo attachmentInfo = GSON.fromJson(attachmentJson, AttachmentInfo.class); + testConfig.pipelineLink = System.getenv("SYSTEM_TEAMFOUNDATIONSERVERURI") + System.getenv("SYSTEM_TEAMPROJECT") + "/_build/results?buildId=" + System.getenv("BUILD_BUILDID"); + printlnf("##[section]Callback pipeline link is: %s", testConfig.pipelineLink); + for (AttachmentInfo attachmentInfo : testConfig.attachmentInfos) { assertTrue(!attachmentInfo.filePath.isEmpty(), "Attachment file " + attachmentInfo.fileName + "has an empty path.", null); File attachment = new File(attachmentInfo.filePath); assertTrue(attachment.exists(), "Attachment file " + attachmentInfo.fileName + "doesn't exist.", null); @@ -196,21 +149,21 @@ private static void runTestInner(String runningType, String appPath, String test printlnf("##[command]Attachment %s uploaded successfully", attachmentInfo.filePath); } - String accessKey = hydraLabAPIClient.generateAccessKey(apiConfig, deviceIdentifier); + String accessKey = hydraLabAPIClient.generateAccessKey(apiConfig, testConfig); if (StringUtils.isEmpty(accessKey)) { printlnf("##[warning]Access key is empty."); } else { printlnf("##[command]Access key obtained."); } - JsonObject responseContent = hydraLabAPIClient.triggerTestRun(runningType, apiConfig, testFileSetId, testSuiteName, deviceIdentifier, accessKey, runTimeoutSec, instrumentationArgs, extraArgs); + JsonObject responseContent = hydraLabAPIClient.triggerTestRun(testConfig, apiConfig, testFileSetId, accessKey); int resultCode = responseContent.get("code").getAsInt(); // retry int waitingRetry = 20; while (resultCode != 200 && waitingRetry > 0) { printlnf("##[warning]Trigger test run failed, remaining retry times: %d\nServer code: %d, message: %s", waitingRetry, resultCode, responseContent.get("message").getAsString()); - responseContent = hydraLabAPIClient.triggerTestRun(runningType, apiConfig, testFileSetId, testSuiteName, deviceIdentifier, accessKey, runTimeoutSec, instrumentationArgs, extraArgs); + responseContent = hydraLabAPIClient.triggerTestRun(testConfig, apiConfig, testFileSetId, accessKey); resultCode = responseContent.get("code").getAsInt(); waitingRetry--; } @@ -219,7 +172,7 @@ private static void runTestInner(String runningType, String appPath, String test String testTaskId = responseContent.getAsJsonObject("content").get("testTaskId").getAsString(); printlnf("##[section]Triggered test task id: %s successful!", testTaskId); - int sleepSecond = runTimeoutSec / 3; + int sleepSecond = testConfig.runTimeOutSeconds / 3; int totalWaitSecond = 0; boolean finished = false; TestTask runningTest = null; @@ -229,14 +182,12 @@ private static void runTestInner(String runningType, String appPath, String test while (!finished) { if (TestTask.TestStatus.WAITING.equals(currentStatus)) { - if (totalWaitSecond > queueTimeoutSec) { - hydraLabAPIClient.cancelTestTask(apiConfig, testTaskId, "Queue timeout!"); - printlnf("Cancelled the task as timeout %d seconds is reached", queueTimeoutSec); + if (totalWaitSecond > testConfig.queueTimeOutSeconds) { break; } printlnf("Get test status after queuing for %d seconds", totalWaitSecond); } else if (TestTask.TestStatus.RUNNING.equals(currentStatus)) { - if (totalWaitSecond > runTimeoutSec) { + if (totalWaitSecond > testConfig.runTimeOutSeconds) { break; } printlnf("Get test status after running for %d seconds", totalWaitSecond); @@ -253,7 +204,7 @@ private static void runTestInner(String runningType, String appPath, String test hydraRetryTime = runningTest.retryTime; printlnf("##[command]Retrying to run task again, current waited second will be reset. current retryTime is: %d", hydraRetryTime); totalWaitSecond = 0; - sleepSecond = runTimeoutSec / 3; + sleepSecond = testConfig.runTimeOutSeconds / 3; } if (TestTask.TestStatus.WAITING.equals(currentStatus)) { @@ -264,7 +215,7 @@ private static void runTestInner(String runningType, String appPath, String test if (TestTask.TestStatus.WAITING.equals(lastStatus)) { printlnf("##[command]Clear waiting time: %d", totalWaitSecond); totalWaitSecond = 0; - sleepSecond = runTimeoutSec / 3; + sleepSecond = testConfig.runTimeOutSeconds / 3; } printlnf("##[command]Running test on %d device, status for now: %s", runningTest.testDevicesCount, currentStatus); assertTrue(!TestTask.TestStatus.CANCELED.equals(currentStatus), "The test task is canceled", runningTest); @@ -283,10 +234,13 @@ private static void runTestInner(String runningType, String appPath, String test } if (TestTask.TestStatus.WAITING.equals(currentStatus)) { - assertTrue(finished, "Queuing timeout after waiting for " + queueTimeoutSec + " seconds! Test id", runningTest); + hydraLabAPIClient.cancelTestTask(apiConfig, testTaskId, "Queue timeout!"); + printlnf("Cancelled the task as queuing timeout %d seconds is reached", testConfig.queueTimeOutSeconds); + assertTrue(finished, "Queuing timeout after waiting for " + testConfig.queueTimeOutSeconds + " seconds! Test id", runningTest); } else if (TestTask.TestStatus.RUNNING.equals(currentStatus)) { hydraLabAPIClient.cancelTestTask(apiConfig, testTaskId, "Run timeout!"); - assertTrue(finished, "Running timeout after waiting for " + runTimeoutSec + " seconds! Test id", runningTest); + printlnf("Cancelled the task as running timeout %d seconds is reached", testConfig.runTimeOutSeconds); + assertTrue(finished, "Running timeout after waiting for " + testConfig.runTimeOutSeconds + " seconds! Test id", runningTest); } assertNotNull(runningTest, "runningTest"); @@ -303,7 +257,7 @@ private static void runTestInner(String runningType, String appPath, String test if (runningTest.totalFailCount > 0) { printlnf("##[error]Fatal error during test, total fail count: %d", runningTest.totalFailCount); - markRunningFail(); + markTestResultFail(); } int index = 0; @@ -314,10 +268,10 @@ private static void runTestInner(String runningType, String appPath, String test // add (test type + timestamp) in folder name to distinguish different test results when using the same device ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC); String testFolder; - if (StringUtils.isEmpty(tag)) { - testFolder = runningType + "-" + utc.format(DateTimeFormatter.ofPattern("MMddHHmmss")); + if (StringUtils.isEmpty(testConfig.artifactTag)) { + testFolder = testConfig.runningType + "-" + utc.format(DateTimeFormatter.ofPattern("MMddHHmmss")); } else { - testFolder = runningType + "-" + tag + "-" + utc.format(DateTimeFormatter.ofPattern("MMddHHmmss")); + testFolder = testConfig.runningType + "-" + testConfig.artifactTag + "-" + utc.format(DateTimeFormatter.ofPattern("MMddHHmmss")); } File file = new File(reportFolderPath, testFolder); diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/YamlParser.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/YamlParser.java new file mode 100644 index 000000000..99a06f76a --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/YamlParser.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package com.microsoft.hydralab.utils; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.hydralab.config.DeviceConfig; +import com.microsoft.hydralab.config.HydraLabAPIConfig; +import com.microsoft.hydralab.config.TestConfig; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.LinkedHashMap; + +/** + * @author Li Shen + * @date 2/9/2023 + */ + +public class YamlParser { + private final LinkedHashMap fileRootMap; + private final ObjectMapper objectMapper; + + public YamlParser(String configFile) throws IOException { + File ymlFile = new File(configFile); + InputStream inputStream = Files.newInputStream(ymlFile.toPath()); + Yaml yaml = new Yaml(); + this.fileRootMap = yaml.load(inputStream); + + this.objectMapper = new ObjectMapper(); + // ignore unknown fields in config yml + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public HydraLabAPIConfig parseAPIConfig() { + Object target = fileRootMap.get("hydraLabAPIServer"); + return objectMapper.convertValue(target, HydraLabAPIConfig.class); + } + + public TestConfig parseTestConfig() { + Object target = fileRootMap.get("testSpec"); + TestConfig testConfig = objectMapper.convertValue(target, TestConfig.class); + testConfig.constructField((HashMap) target); + if (testConfig.deviceConfig != null) { + testConfig.deviceConfig.extractFromExistingField(); + } + return testConfig; + } +} diff --git a/gradle_plugin/src/main/resources/template/build.gradle b/gradle_plugin/src/main/resources/template/build.gradle deleted file mode 100644 index bc8fefa9e..000000000 --- a/gradle_plugin/src/main/resources/template/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -buildscript { - repositories { - maven { - url "https://plugins.gradle.org/m2/" - } - google() - } - dependencies { - classpath 'com.microsoft.hydralab:gradle_plugin:${version}' - } -} - -apply plugin: "com.microsoft.hydralab.client-util" \ No newline at end of file diff --git a/gradle_plugin/src/test/java/com/microsoft/hydralab/ClientUtilsPluginTest.java b/gradle_plugin/src/test/java/com/microsoft/hydralab/ClientUtilsPluginTest.java index 4fdfd27ec..196edfce8 100644 --- a/gradle_plugin/src/test/java/com/microsoft/hydralab/ClientUtilsPluginTest.java +++ b/gradle_plugin/src/test/java/com/microsoft/hydralab/ClientUtilsPluginTest.java @@ -3,9 +3,10 @@ package com.microsoft.hydralab; import com.google.gson.JsonObject; -import com.microsoft.hydralab.entity.AttachmentInfo; -import com.microsoft.hydralab.entity.HydraLabAPIConfig; -import com.microsoft.hydralab.entity.TestTask; +import com.microsoft.hydralab.config.DeviceConfig; +import com.microsoft.hydralab.config.HydraLabAPIConfig; +import com.microsoft.hydralab.config.TestConfig; +import com.microsoft.hydralab.entity.*; import com.microsoft.hydralab.utils.HydraLabAPIClient; import com.microsoft.hydralab.utils.HydraLabClientUtils; import org.junit.jupiter.api.Assertions; @@ -24,141 +25,155 @@ public class ClientUtilsPluginTest { ClientUtilsPlugin clientUtilsPlugin = new ClientUtilsPlugin(); - String appPath = "src/test/resources/app.txt"; - - String testAppPath = "src/test/resources/test_app.txt"; - @Test public void checkGeneralTestRequiredParam() { - String runningType = ""; - String appPath = ""; - String deviceIdentifier = ""; - String runTimeOutSeconds = ""; HydraLabAPIConfig apiConfig = new HydraLabAPIConfig(); - apiConfig.pkgName = ""; + TestConfig testConfig = new TestConfig(); + DeviceConfig deviceConfig = new DeviceConfig(); + testConfig.deviceConfig = deviceConfig; + + deviceConfig.deviceIdentifier = ""; + testConfig.runningType = ""; + testConfig.appPath = ""; + testConfig.runTimeOutSeconds = 0; apiConfig.authToken = ""; - String testAppPath = "./testAppPath/testApp.apk"; - String testSuiteName = "com.example.test.suite"; - apiConfig.testPkgName = "TestPkgName"; - apiConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; + testConfig.pkgName = ""; + testConfig.testAppPath = "./testAppPath/testApp.apk"; + testConfig.testPkgName = "TestPkgName"; + testConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; + testConfig.testSuiteName = "com.example.test.suite"; - generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + generalParamCheck(apiConfig, testConfig); - runningType = "INSTRUMENTATION"; - generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + apiConfig.host = "www.test.host"; + generalParamCheck(apiConfig, testConfig); - appPath = "./appPath/app.apk"; - generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + apiConfig.authToken = "thisisanauthtokenonlyfortest"; + generalParamCheck(apiConfig, testConfig); - deviceIdentifier = "TESTDEVICESN001"; - generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + testConfig.appPath = "./appPath/app.apk"; + generalParamCheck(apiConfig, testConfig); - runTimeOutSeconds = "1000"; - generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + testConfig.pkgName = "PkgName"; + generalParamCheck(apiConfig, testConfig); - apiConfig.pkgName = "PkgName"; - generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + testConfig.runningType = "INSTRUMENTATION"; + generalParamCheck(apiConfig, testConfig); - apiConfig.authToken = "thisisanauthtokenonlyfortest"; - clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + testConfig.runTimeOutSeconds = 1000; + generalParamCheck(apiConfig, testConfig); + + deviceConfig.deviceIdentifier = "TESTDEVICESN001"; + clientUtilsPlugin.requiredParamCheck(apiConfig, testConfig); } @Test public void checkInstrumentationTestRequiredParam() { - String runningType = "INSTRUMENTATION"; - String appPath = "./appPath/app.apk"; - String deviceIdentifier = "TESTDEVICESN001"; - String runTimeOutSeconds = "1000"; HydraLabAPIConfig apiConfig = new HydraLabAPIConfig(); - apiConfig.pkgName = "PkgName"; + TestConfig testConfig = new TestConfig(); + DeviceConfig deviceConfig = new DeviceConfig(); + testConfig.deviceConfig = deviceConfig; + + testConfig.runningType = "INSTRUMENTATION"; + testConfig.appPath = "./appPath/app.apk"; + testConfig.runTimeOutSeconds = 1000; + testConfig.pkgName = "PkgName"; + testConfig.testAppPath = ""; + testConfig.testSuiteName = ""; + testConfig.testPkgName = ""; + testConfig.testScope = ""; + apiConfig.host = "www.test.host"; apiConfig.authToken = "thisisanauthtokenonlyfortest"; - String testAppPath = ""; - String testSuiteName = ""; - apiConfig.testPkgName = ""; - apiConfig.testScope = ""; + deviceConfig.deviceIdentifier = "TESTDEVICESN001"; - typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testAppPath"); - testAppPath = "./testAppPath/testApp.apk"; + typeSpecificParamCheck(apiConfig, testConfig, "testAppPath"); + testConfig.testAppPath = "./testAppPath/testApp.apk"; - typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testPkgName"); - apiConfig.testPkgName = "TestPkgName"; + typeSpecificParamCheck(apiConfig, testConfig, "testPkgName"); + testConfig.testPkgName = "TestPkgName"; - apiConfig.testScope = ClientUtilsPlugin.TestScope.TEST_APP; - clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + testConfig.testScope = ClientUtilsPlugin.TestScope.TEST_APP; + clientUtilsPlugin.requiredParamCheck(apiConfig, testConfig); - apiConfig.testScope = ClientUtilsPlugin.TestScope.PACKAGE; - typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testSuiteName"); + testConfig.testScope = ClientUtilsPlugin.TestScope.PACKAGE; + typeSpecificParamCheck(apiConfig, testConfig, "testSuiteName"); - apiConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; - typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testSuiteName"); + testConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; + typeSpecificParamCheck(apiConfig, testConfig, "testSuiteName"); - apiConfig.testScope = ClientUtilsPlugin.TestScope.PACKAGE; - testSuiteName = "com.example.test.suite"; - clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + testConfig.testScope = ClientUtilsPlugin.TestScope.PACKAGE; + testConfig.testSuiteName = "com.example.test.suite"; + clientUtilsPlugin.requiredParamCheck(apiConfig, testConfig); - apiConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; - clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + testConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; + clientUtilsPlugin.requiredParamCheck(apiConfig, testConfig); } @Test public void checkAppiumTestRequiredParam() { - String runningType = "APPIUM"; - String appPath = "./appPath/app.apk"; - String deviceIdentifier = "TESTDEVICESN001"; - String runTimeOutSeconds = "1000"; HydraLabAPIConfig apiConfig = new HydraLabAPIConfig(); - apiConfig.pkgName = "PkgName"; + TestConfig testConfig = new TestConfig(); + DeviceConfig deviceConfig = new DeviceConfig(); + testConfig.deviceConfig = deviceConfig; + + testConfig.runningType = "APPIUM"; + testConfig.appPath = "./appPath/app.apk"; + testConfig.runTimeOutSeconds = 1000; + testConfig.pkgName = "PkgName"; + testConfig.testAppPath = ""; + testConfig.testSuiteName = ""; + apiConfig.host = "www.test.host"; apiConfig.authToken = "thisisanauthtokenonlyfortest"; - String testAppPath = ""; - String testSuiteName = ""; + deviceConfig.deviceIdentifier = "TESTDEVICESN001"; - typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testAppPath"); - testAppPath = "./testAppPath/testApp.apk"; + typeSpecificParamCheck(apiConfig, testConfig, "testAppPath"); + testConfig.testAppPath = "./testAppPath/testApp.apk"; - typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testSuiteName"); - testSuiteName = "com.example.test.suite"; + typeSpecificParamCheck(apiConfig, testConfig, "testSuiteName"); + testConfig.testSuiteName = "com.example.test.suite"; - clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + clientUtilsPlugin.requiredParamCheck(apiConfig, testConfig); } @Test public void checkAppiumCrossTestRequiredParam() { - String runningType = "APPIUM_CROSS"; - String appPath = "./appPath/app.apk"; - String deviceIdentifier = "TESTDEVICESN001"; - String runTimeOutSeconds = "1000"; HydraLabAPIConfig apiConfig = new HydraLabAPIConfig(); - apiConfig.pkgName = "PkgName"; + TestConfig testConfig = new TestConfig(); + DeviceConfig deviceConfig = new DeviceConfig(); + testConfig.deviceConfig = deviceConfig; + + testConfig.runningType = "APPIUM"; + testConfig.appPath = "./appPath/app.apk"; + testConfig.runTimeOutSeconds = 1000; + testConfig.pkgName = "PkgName"; + testConfig.testAppPath = ""; + testConfig.testSuiteName = ""; + apiConfig.host = "www.test.host"; apiConfig.authToken = "thisisanauthtokenonlyfortest"; - String testAppPath = ""; - String testSuiteName = ""; + deviceConfig.deviceIdentifier = "TESTDEVICESN001"; - typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testAppPath"); - testAppPath = "./testAppPath/testApp.apk"; + typeSpecificParamCheck(apiConfig, testConfig, "testAppPath"); + testConfig.testAppPath = "./testAppPath/testApp.apk"; - typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testSuiteName"); - testSuiteName = "com.example.test.suite"; + typeSpecificParamCheck(apiConfig, testConfig, "testSuiteName"); + testConfig.testSuiteName = "com.example.test.suite"; - clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + clientUtilsPlugin.requiredParamCheck(apiConfig, testConfig); } @Test public void runTestOnDeviceWithApp() { - String runningType = "INSTRUMENTATION"; - String attachmentConfigPath = ""; - String testSuiteName = "com.example.test.suite"; - String deviceIdentifier = "TESTDEVICESN001"; - int queueTimeoutSec = 1000; - int runTimeoutSec = 1000; String reportFolderPath = "./reportFolder"; - Map instrumentationArgs = new HashMap<>(); - Map extraArgs = new HashMap<>(); - String tag = ""; - HydraLabAPIConfig apiConfig = Mockito.mock(HydraLabAPIConfig.class); HydraLabAPIClient client = Mockito.mock(HydraLabAPIClient.class); + HydraLabAPIConfig apiConfig = Mockito.mock(HydraLabAPIConfig.class); + TestConfig testConfig = Mockito.mock(TestConfig.class); + testConfig.runningType = "INSTRUMENTATION"; + testConfig.appPath = "src/test/resources/app.txt"; + testConfig.testAppPath = "src/test/resources/test_app.txt"; + testConfig.attachmentInfos = new ArrayList<>(); String returnId = "id123456"; - when(client.uploadApp(Mockito.any(HydraLabAPIConfig.class), Mockito.anyString(), Mockito.anyString(), + when(client.uploadApp(Mockito.any(HydraLabAPIConfig.class), Mockito.any(TestConfig.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.any(File.class), Mockito.any(File.class))) .thenReturn(returnId); @@ -169,7 +184,7 @@ public void runTestOnDeviceWithApp() { Mockito.any(AttachmentInfo.class), Mockito.any(File.class))) .thenReturn(returnJson); - when(client.generateAccessKey(Mockito.any(HydraLabAPIConfig.class), Mockito.anyString())) + when(client.generateAccessKey(Mockito.any(HydraLabAPIConfig.class), Mockito.any(TestConfig.class))) .thenReturn("accessKey"); returnJson = new JsonObject(); @@ -179,8 +194,7 @@ public void runTestOnDeviceWithApp() { subJsonObject.addProperty("devices", "device1,device2"); subJsonObject.addProperty("testTaskId", "test_task_id"); returnJson.add("content", subJsonObject); - when(client.triggerTestRun(Mockito.anyString(), Mockito.any(HydraLabAPIConfig.class), Mockito.anyString(), Mockito.anyString(), - Mockito.anyString(), Mockito.anyString(), Mockito.anyInt(), Mockito.anyMap(), Mockito.anyMap())) + when(client.triggerTestRun(Mockito.any(TestConfig.class), Mockito.any(HydraLabAPIConfig.class), Mockito.anyString(), Mockito.anyString())) .thenReturn(returnJson); TestTask returnTestTask = new TestTask(); @@ -201,9 +215,7 @@ public void runTestOnDeviceWithApp() { .thenReturn(returnBlobSAS); HydraLabClientUtils.switchClientInstance(client); - HydraLabClientUtils.runTestOnDeviceWithApp(runningType, appPath, testAppPath, attachmentConfigPath, - testSuiteName, deviceIdentifier, queueTimeoutSec, runTimeoutSec, reportFolderPath, instrumentationArgs, - extraArgs, tag, apiConfig); + HydraLabClientUtils.runTestOnDeviceWithApp(reportFolderPath, apiConfig, testConfig); verify(client, times(0)).cancelTestTask(Mockito.any(HydraLabAPIConfig.class), Mockito.anyString(), Mockito.anyString()); verify(client, times(0)).downloadToFile(Mockito.anyString(), Mockito.any(File.class)); @@ -228,17 +240,17 @@ public void getLatestCommitInfo() { Assertions.assertNotNull(commitMsg, "Get commit message error"); } - private void generalParamCheck(String appPath, String deviceIdentifier, String runTimeOutSeconds, HydraLabAPIConfig apiConfig, String testAppPath, String testSuiteName, String runningType) { + private void generalParamCheck(HydraLabAPIConfig apiConfig, TestConfig testConfig) { IllegalArgumentException thrown = Assertions.assertThrows(IllegalArgumentException.class, () -> { - clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + clientUtilsPlugin.requiredParamCheck(apiConfig, testConfig); }, "IllegalArgumentException was expected"); - Assertions.assertEquals("Required params not provided! Make sure the following params are all provided correctly: authToken, appPath, pkgName, runningType, deviceIdentifier, runTimeOutSeconds.", thrown.getMessage()); + Assertions.assertEquals("Required params not provided! Make sure the following params are all provided correctly: hydraLabAPIhost, authToken, deviceIdentifier, appPath, pkgName, runningType, runTimeOutSeconds.", thrown.getMessage()); } - private void typeSpecificParamCheck(String appPath, String deviceIdentifier, String runTimeOutSeconds, HydraLabAPIConfig apiConfig, String testAppPath, String testSuiteName, String runningType, String requiredParamName) { + private void typeSpecificParamCheck(HydraLabAPIConfig apiConfig, TestConfig testConfig, String requiredParamName) { IllegalArgumentException thrown = Assertions.assertThrows(IllegalArgumentException.class, () -> { - clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + clientUtilsPlugin.requiredParamCheck(apiConfig, testConfig); }, "IllegalArgumentException was expected"); - Assertions.assertEquals("Required param " + requiredParamName + " not provided!", thrown.getMessage()); + Assertions.assertEquals("Running type " + testConfig.runningType + " required param " + requiredParamName + " not provided!", thrown.getMessage()); } } \ No newline at end of file diff --git a/gradle_plugin/template/build.gradle b/gradle_plugin/template/build.gradle new file mode 100644 index 000000000..9df8409a7 --- /dev/null +++ b/gradle_plugin/template/build.gradle @@ -0,0 +1,22 @@ +/** + * Select one from the following approaches you apply plugins. + */ + +// Using the plugins DSL: +plugins { + id "com.microsoft.hydralab.client-util" version "${plugin_version}" +} + +// Using legacy plugin application: +buildscript { + repositories { + maven { + url "https://plugins.gradle.org/m2/" + } + google() + } + dependencies { + classpath 'com.microsoft.hydralab:gradle_plugin:${plugin_version}' + } +} +apply plugin: "com.microsoft.hydralab.client-util" \ No newline at end of file diff --git a/gradle_plugin/src/main/resources/template/gradle.properties b/gradle_plugin/template/gradle.properties similarity index 81% rename from gradle_plugin/src/main/resources/template/gradle.properties rename to gradle_plugin/template/gradle.properties index eacc5522d..9a20ec2b2 100644 --- a/gradle_plugin/src/main/resources/template/gradle.properties +++ b/gradle_plugin/template/gradle.properties @@ -5,10 +5,13 @@ deviceIdentifier = # Required, identifier of the device / group of devices for r queueTimeOutSeconds = # Required, timeout(in seconds) threshold of waiting the tests to be started when target devices are under TESTING. runTimeOutSeconds = # Required, timeout(in seconds) threshold of running the tests. -# SINGLE: a single device specified by param deviceIdentifier;; +# @Deprecated, use param "triggerType" instead in the latest version. +type = # Optional, how the test is triggered, currently the value is set with $(Build.Reason) from ADO pipeline, or default to be "API". Value: {API (Default), $(Build.Reason)} +# SINGLE: a single device specified by param deviceIdentifier; # REST: rest devices in the group specified by param deviceIdentifier; # ALL: all devices in the group specified by param deviceIdentifier; groupTestType = # Optional, Value: {SINGLE (Default), REST, ALL} +# @Deprecated, use param "testRunArgs" instead in the latest version. instrumentationArgs = # Optional, All extra params. Example: "a1=x1|x2,b1=x3|x4|x5,c1=x6" will pass variables '{"a1": "x1,x2", "b1": "x3,x4,x5", "c1": "x6"}' # Optional, path to JSON config file that is used for attachment uploading. File content should be in the following schema: @@ -26,13 +29,16 @@ attachmentConfigPath = neededPermissions = # Optional, list of permission names that the test requires, separated by comma. Example: "android.Permission1, android.Permission2" # Optional, list of actions that the test will operate on the device, content should be in format of a JSON string. +# (In yml config file, this param is assigned with value directly by hierarchy) # 1. Current support actions for during setting up and tearing down, keys of the first level can be selected from: setUp | tearDown. The value of them should both be a JSON array. -# 2. Method types, as the value of key "method", can be selected from a currently supporting list ["setProperty", "setDefaultLauncher", "backToHome", "changeGlobalSetting", "changeSystemSetting"]; -# 3. Provide corresponding params for target methods. +# 2. Device types, as the value of key "deviceType", currently can be selected from a supporting list ["Android", "iOS", etc...]. This key is optional. +# 3. Method types, as the value of key "method", currently can be selected from a supporting list ["setProperty", "setDefaultLauncher", "backToHome", "changeGlobalSetting", "changeSystemSetting"]; +# 4. Provide corresponding params for target methods. # See more details in Hydra Lab wiki (section: TBD). -# Example: "{\"setUp\":[{\"method\":\"setProperty\",\"args\":[\"value A\", \"value B\"]}, {...}, {...}], \"tearDown\":[{\"method\":\"backToHome\",\"args\":[]}, {...}, {...}]}" +# Example: "{\"setUp\":[{\"deviceType\":\"Android\",\"method\":\"setProperty\",\"args\":[\"value A\", \"value B\"]}, {...}, {...}], \"tearDown\":[{\"method\":\"backToHome\",\"args\":[]}, {...}, {...}]}" deviceActions = +# @Deprecated, use param "artifactTag" instead in the latest version. # Optional, used to change test result folder name prefix. Is commonly added when artifact folder is used for specific approaches. # Normal result folder name: $(runningType)-$(dateTime) # Result folder name with tag: $(runningType)-$(tag)-$(dateTime) @@ -72,6 +78,7 @@ testPkgName = # Absolute package name of the test app. # Optional for test type: SMART, APPIUM_MONKEY maxStepCount = # The max step count for each SMART test. +# @Deprecated, use param "testRound" instead in the latest version. # Optional for test type: SMART deviceTestCount = # The number of times to run SMART test. diff --git a/gradle_plugin/template/testSpec.yml b/gradle_plugin/template/testSpec.yml new file mode 100644 index 000000000..8ea1e2504 --- /dev/null +++ b/gradle_plugin/template/testSpec.yml @@ -0,0 +1,52 @@ +# [IMPORTANT] Clean keys with no value, otherwise default value would be overlapped with null. +# See detailed explanation for parameters in https://github.com/microsoft/HydraLab/blob/main/gradle_plugin/template/gradle.properties + +hydraLabAPIServer: + host: # + schema: # + authToken: # + +testSpec: + device: + deviceIdentifier: # + groupTestType: # + deviceActions: # + : # + - method: # + args: # + - xxx + - xxx + - xxx + triggerType: # + runningType: # + appPath: # + pkgName: # + testAppPath: # + testPkgName: # + teamName: # + testRunnerName: # + testScope: # + testSuiteName: # + frameworkType: # + runTimeOutSeconds: # + queueTimeOutSeconds: # + needUninstall: # + needClearData: # + neededPermissions: # + - xxx + - xxx + # : usage priority: attachmentConfigPath > attachmentInfos + attachmentConfigPath: # + attachmentInfos: # + - fileName: + filePath: + fileType: + loadType: + loadDir: + testRunArgs: # +# key1: value1 +# key2: value2 + artifactTag: # + exploration: + maxStepCount: # + testRound: # \ No newline at end of file