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 000000000..2f687e59c Binary files /dev/null and b/docs/images/UML/gradle_plugin_yaml_config_design.png differ 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