From 4d2061a44a24c171c39b140d35eaa2f7edba66a6 Mon Sep 17 00:00:00 2001 From: OliverShen <41765758+olivershen-wow@users.noreply.github.com> Date: Mon, 16 Jan 2023 15:47:35 +0800 Subject: [PATCH] Update Gradle plugin to add neededPermissions and deviceActions (#226) * Update Gradle plugin to add neededPermissions and deviceActions; Upgrade plugin to 1.0.43; --- gradle_plugin/build.gradle | 3 +- .../hydralab/ClientUtilsPlugin.groovy | 28 +- .../hydralab/entity/AttachmentInfo.java | 9 + .../hydralab/entity/BlobFileInfo.java | 33 + .../hydralab/entity/DeviceTestResult.java | 38 ++ .../hydralab/entity/HydraLabAPIConfig.java | 98 +++ .../microsoft/hydralab/entity/TestTask.java | 39 ++ .../microsoft/hydralab/utils/CommonUtils.java | 76 +++ .../hydralab/utils/HydraLabAPIClient.java | 385 +++++++++++ .../hydralab/utils/HydraLabClientUtils.java | 640 +----------------- .../main/resources/template/gradle.properties | 17 + .../hydralab/ClientUtilsPluginTest.java | 244 +++++++ .../utils/HydraLabClientUtilsTest.java | 34 - gradle_plugin/src/test/resources/app.txt | 0 gradle_plugin/src/test/resources/test_app.txt | 0 15 files changed, 993 insertions(+), 651 deletions(-) create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/AttachmentInfo.java create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/BlobFileInfo.java create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceTestResult.java create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/HydraLabAPIConfig.java create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/TestTask.java create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/CommonUtils.java create mode 100644 gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabAPIClient.java create mode 100644 gradle_plugin/src/test/java/com/microsoft/hydralab/ClientUtilsPluginTest.java delete mode 100644 gradle_plugin/src/test/java/com/microsoft/hydralab/utils/HydraLabClientUtilsTest.java create mode 100644 gradle_plugin/src/test/resources/app.txt create mode 100644 gradle_plugin/src/test/resources/test_app.txt diff --git a/gradle_plugin/build.gradle b/gradle_plugin/build.gradle index 97172c069..86b59c5b4 100644 --- a/gradle_plugin/build.gradle +++ b/gradle_plugin/build.gradle @@ -24,6 +24,7 @@ ext { } dependencies { + testImplementation 'org.mockito:mockito-core:3.12.4' implementation "org.apache.commons:commons-lang3:3.4" implementation gradleApi() implementation "commons-io:commons-io:2.11.0" @@ -34,7 +35,7 @@ dependencies { } // plugin publishing related -version = '1.0.41' +version = '1.0.45' 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/src/main/groovy/com/microsoft/hydralab/ClientUtilsPlugin.groovy b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/ClientUtilsPlugin.groovy index 8bd21b202..5176d8068 100644 --- a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/ClientUtilsPlugin.groovy +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/ClientUtilsPlugin.groovy @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.microsoft.hydralab +import com.microsoft.hydralab.entity.HydraLabAPIConfig import com.microsoft.hydralab.utils.HydraLabClientUtils import org.apache.commons.lang3.StringUtils import org.gradle.api.Plugin @@ -102,7 +103,7 @@ class ClientUtilsPlugin implements Plugin { } } - HydraLabClientUtils.HydraLabAPIConfig apiConfig = HydraLabClientUtils.HydraLabAPIConfig.defaultAPI() + HydraLabAPIConfig apiConfig = new HydraLabAPIConfig() if (project.hasProperty('hydraLabAPISchema')) { apiConfig.schema = project.hydraLabAPISchema @@ -149,6 +150,13 @@ class ClientUtilsPlugin implements Plugin { if (project.hasProperty('needClearData')) { apiConfig.needClearData = Boolean.parseBoolean(project.needClearData) } + if (project.hasProperty('neededPermissions')) { + apiConfig.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("\\", "\"") + } requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig) @@ -165,7 +173,7 @@ class ClientUtilsPlugin implements Plugin { } } - private void requiredParamCheck(String runningType, String appPath, String testAppPath, String deviceIdentifier, String runTimeOutSeconds, String testSuiteName, HydraLabClientUtils.HydraLabAPIConfig apiConfig) { + 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) @@ -173,39 +181,39 @@ class ClientUtilsPlugin implements Plugin { || StringUtils.isBlank(runTimeOutSeconds) || StringUtils.isBlank(apiConfig.authToken) ) { - throw new Exception('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: authToken, appPath, pkgName, runningType, deviceIdentifier, runTimeOutSeconds.') } // running type specified params switch (runningType) { case "INSTRUMENTATION": if (StringUtils.isBlank(testAppPath)) { - throw new Exception('Required param testAppPath not provided!') + throw new IllegalArgumentException('Required param testAppPath not provided!') } if (StringUtils.isBlank(apiConfig.testPkgName)) { - throw new Exception('Required param testPkgName not provided!') + throw new IllegalArgumentException('Required param testPkgName not provided!') } if (apiConfig.testScope != TestScope.PACKAGE && apiConfig.testScope != TestScope.CLASS) { break } if (StringUtils.isBlank(testSuiteName)) { - throw new Exception('Required param testSuiteName not provided!') + throw new IllegalArgumentException('Required param testSuiteName not provided!') } break case "APPIUM": if (StringUtils.isBlank(testAppPath)) { - throw new Exception('Required param testAppPath not provided!') + throw new IllegalArgumentException('Required param testAppPath not provided!') } if (StringUtils.isBlank(testSuiteName)) { - throw new Exception('Required param testSuiteName not provided!') + throw new IllegalArgumentException('Required param testSuiteName not provided!') } break case "APPIUM_CROSS": if (StringUtils.isBlank(testAppPath)) { - throw new Exception('Required param testAppPath not provided!') + throw new IllegalArgumentException('Required param testAppPath not provided!') } if (StringUtils.isBlank(testSuiteName)) { - throw new Exception('Required param testSuiteName not provided!') + throw new IllegalArgumentException('Required param testSuiteName not provided!') } break case "SMART": 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 new file mode 100644 index 000000000..81685c124 --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/AttachmentInfo.java @@ -0,0 +1,9 @@ +package com.microsoft.hydralab.entity; + +public class AttachmentInfo { + public String fileName; + public String filePath; + public String fileType; + public String loadType; + public String loadDir; +} 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 new file mode 100644 index 000000000..a7a7299ce --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/BlobFileInfo.java @@ -0,0 +1,33 @@ +package com.microsoft.hydralab.entity; + +import com.google.gson.JsonObject; +import java.util.Date; + +public class BlobFileInfo { + public String fileId; + public String fileType; + public String fileName; + public String blobUrl; + public String blobPath; + public long fileLen; + public String md5; + public String loadDir; + public String loadType; + public JsonObject fileParser; + public Date createTime; + public Date updateTime; + + + public interface fileType { + String WINDOWS_APP = "WINAPP"; + String COMMOM_FILE = "COMMON"; + String AGENT_PACKAGE = "PACKAGE"; + String APP_FILE = "APP"; + String TEST_APP_FILE = "TEST_APP"; + } + + public interface loadType { + String CPOY = "COPY"; + String UNZIP = "UNZIP"; + } +} \ 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 new file mode 100644 index 000000000..749cf189e --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/DeviceTestResult.java @@ -0,0 +1,38 @@ +package com.microsoft.hydralab.entity; + +import java.util.List; + +public class DeviceTestResult { + public String id; + public String deviceSerialNumber; + public String deviceName; + public String instrumentReportPath; + public String controlLogPath; + public String instrumentReportBlobUrl; + public String testXmlReportBlobUrl; + public String logcatBlobUrl; + public String testGifBlobUrl; + + public List attachments; + + public String crashStackId; + public String errorInProcess; + + public String crashStack; + + public int totalCount; + public int failCount; + public boolean success; + public long testStartTimeMillis; + public long testEndTimeMillis; + + @Override + public String toString() { + return "{" + + "SN='" + deviceSerialNumber + '\'' + + ", totalCase:" + totalCount + + ", failCase:" + failCount + + ", success:" + success + + '}'; + } +} diff --git a/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/HydraLabAPIConfig.java b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/HydraLabAPIConfig.java new file mode 100644 index 000000000..b6f98b94e --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/HydraLabAPIConfig.java @@ -0,0 +1,98 @@ +package com.microsoft.hydralab.entity; + +import java.util.ArrayList; +import java.util.List; +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"; + public String addAttachmentAPIPath = "/api/package/addAttachment"; + public String generateAccessKeyAPIPath = "/api/deviceGroup/generate?deviceIdentifier=%s"; + public String runTestAPIPath = "/api/test/task/run/"; + public String testStatusAPIPath = "/api/test/task/"; + 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); + } + + public String checkCenterAliveUrl() { + return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, checkCenterAliveAPIPath); + } + + public String getUploadUrl() { + return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, uploadAPKAPIPath); + } + + public String getAddAttachmentUrl() { + return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, addAttachmentAPIPath); + } + + public String getGenerateAccessKeyUrl() { + return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, generateAccessKeyAPIPath); + } + + public String getRunTestUrl() { + return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, runTestAPIPath); + } + + public String getTestStatusUrl(String testTaskId) { + return String.format(Locale.US, "%s://%s%s%s%s", schema, host, contextPath, testStatusAPIPath, testTaskId); + } + + public String getCancelTestTaskUrl() { + return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, cancelTestTaskAPIPath); + } + + public String getTestReportUrl(String testTaskId) { + return String.format(Locale.US, "%s://%s%s%s%s", schema, host, contextPath, testPortalTaskInfoPath, testTaskId); + } + + public String getDeviceTestVideoUrl(String id) { + return String.format(Locale.US, "%s://%s%s%s%s", schema, host, contextPath, testPortalTaskDeviceVideoPath, 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; + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..767e603cc --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/entity/TestTask.java @@ -0,0 +1,39 @@ +package com.microsoft.hydralab.entity; + +import java.util.Date; +import java.util.List; + +public class TestTask { + public String id; + public List deviceTestResults; + public int testDevicesCount; + public Date startDate; + public Date endDate; + public int totalTestCount; + public int totalFailCount; + public String testSuite; + public String reportImagePath; + public String status; + public String testErrorMsg; + public String message; + public int retryTime; + + @Override + public String toString() { + return "TestTask{" + + "id='" + id + '\'' + + ", testDevicesCount=" + testDevicesCount + + ", startDate=" + startDate + + ", totalTestCount=" + totalTestCount + + ", status='" + status + '\'' + + '}'; + } + + public interface TestStatus { + String RUNNING = "running"; + String FINISHED = "finished"; + String CANCELED = "canceled"; + String EXCEPTION = "error"; + String WAITING = "waiting"; + } +} 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 new file mode 100644 index 000000000..cfe7305af --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/CommonUtils.java @@ -0,0 +1,76 @@ +package com.microsoft.hydralab.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CommonUtils { + public static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(Date.class, new TypeAdapter() { + @Override + public void write(JsonWriter out, Date value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.getTime()); + } + } + + @Override + public Date read(JsonReader in) throws IOException { + if (in != null) { + try { + return new Date(in.nextLong()); + } catch (IllegalStateException e) { + in.nextNull(); + return null; + } + } else { + return null; + } + } + }).create(); + + public static String maskCred(String content) { + for (HydraLabClientUtils.MaskSensitiveData sensitiveData : HydraLabClientUtils.MaskSensitiveData.values()) { + Pattern PATTERNCARD = Pattern.compile(sensitiveData.getRegEx(), Pattern.CASE_INSENSITIVE); + Matcher matcher = PATTERNCARD.matcher(content); + if (matcher.find()) { + String maskedMessage = matcher.group(2); + if (maskedMessage.length() > 0) { + content = content.replaceFirst(maskedMessage, "***"); + } + } + } + + return content; + } + + public static void printlnf(String format, Object... args) { + ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC); + System.out.print("[" + utc.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "] "); + System.out.printf(format + "\n", args); + } + + public static void assertNotNull(Object notnull, String argName) { + if (notnull == null) { + throw new IllegalArgumentException(argName + " is null"); + } + } + + public static void assertTrue(boolean beTrue, String msg, Object data) { + if (!beTrue) { + throw new IllegalStateException(msg + (data == null ? "" : ": " + data)); + } + } +} 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 new file mode 100644 index 000000000..cf06258f3 --- /dev/null +++ b/gradle_plugin/src/main/groovy/com/microsoft/hydralab/utils/HydraLabAPIClient.java @@ -0,0 +1,385 @@ +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 okhttp3.*; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.microsoft.hydralab.utils.CommonUtils.*; + + +public class HydraLabAPIClient { + private final OkHttpClient client = new OkHttpClient.Builder() + .readTimeout(300, TimeUnit.SECONDS) + .connectTimeout(300, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); + + private final int httpFailureRetryTimes = 10; + + public void checkCenterAlive(HydraLabAPIConfig apiConfig) { + Request req = new Request.Builder() + .addHeader("Authorization", "Bearer " + apiConfig.authToken) + .url(apiConfig.checkCenterAliveUrl()) + .build(); + OkHttpClient clientToUse = client; + Response response = null; + try { + response = clientToUse.newCall(req).execute(); + int waitingRetry = httpFailureRetryTimes; + while (!response.isSuccessful() && waitingRetry > 0) { + printlnf("##[warning]Check center alive failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); + response = clientToUse.newCall(req).execute(); + waitingRetry--; + } + + assertTrue(response.isSuccessful(), "check center alive", response); + printlnf("Center is alive, continue on requesting API..."); + } catch (Exception e) { + throw new RuntimeException("check center alive fail: " + e.getMessage(), e); + } finally { + response.close(); + } + } + + public String uploadApp(HydraLabAPIConfig apiConfig, String commitId, String commitCount, String commitMsg, File app, File testApp) { + checkCenterAlive(apiConfig); + + MediaType contentType = MediaType.get("application/vnd.android.package-archive"); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("commitId", commitId) + .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 (testApp != null) { + multipartBodyBuilder.addFormDataPart("testAppFile", testApp.getName(), RequestBody.create(contentType, testApp)); + } + + Request req = new Request.Builder() + .addHeader("Authorization", "Bearer " + apiConfig.authToken) + .url(apiConfig.getUploadUrl()) + .post(multipartBodyBuilder.build()) + .build(); + OkHttpClient clientToUse = client; + Response response = null; + try { + response = clientToUse.newCall(req).execute(); + int waitingRetry = httpFailureRetryTimes; + while (!response.isSuccessful() && waitingRetry > 0) { + printlnf("##[warning]Upload App failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); + response = clientToUse.newCall(req).execute(); + waitingRetry--; + } + + assertTrue(response.isSuccessful(), "upload App", response); + ResponseBody body = response.body(); + + assertNotNull(body, response + ": upload App ResponseBody"); + JsonObject jsonObject = GSON.fromJson(body.string(), JsonObject.class); + + int resultCode = jsonObject.get("code").getAsInt(); + assertTrue(resultCode == 200, "Server returned code: " + resultCode, jsonObject); + + return jsonObject.getAsJsonObject("content").get("id").getAsString(); + } catch (Exception e) { + throw new RuntimeException("upload App fail: " + e.getMessage(), e); + } finally { + response.close(); + } + } + + public JsonObject addAttachment(HydraLabAPIConfig apiConfig, String testFileSetId, AttachmentInfo attachmentConfig, File attachment) { + checkCenterAlive(apiConfig); + + // default text file type: text/plain + // default binary file type: application/octet-stream + // todo: check if file is readable, set corresponding type + MediaType contentType = MediaType.get("application/octet-stream"); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("fileSetId", testFileSetId) + .addFormDataPart("fileType", attachmentConfig.fileType) + .addFormDataPart("attachment", attachmentConfig.fileName, RequestBody.create(contentType, attachment)); + if (StringUtils.isNotEmpty(attachmentConfig.loadType)) { + multipartBodyBuilder.addFormDataPart("loadType", attachmentConfig.loadType); + } + if (StringUtils.isNotEmpty(attachmentConfig.loadDir)) { + multipartBodyBuilder.addFormDataPart("loadDir", attachmentConfig.loadDir); + } + + Request req = new Request.Builder() + .addHeader("Authorization", "Bearer " + apiConfig.authToken) + .url(apiConfig.getAddAttachmentUrl()) + .post(multipartBodyBuilder.build()) + .build(); + OkHttpClient clientToUse = client; + Response response = null; + try { + response = clientToUse.newCall(req).execute(); + int waitingRetry = httpFailureRetryTimes; + while (!response.isSuccessful() && waitingRetry > 0) { + printlnf("##[warning]Add attachments failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); + response = clientToUse.newCall(req).execute(); + waitingRetry--; + } + + assertTrue(response.isSuccessful(), "Add attachments", response); + ResponseBody body = response.body(); + assertNotNull(body, response + ": Add attachments ResponseBody"); + + return GSON.fromJson(body.string(), JsonObject.class); + } catch (Exception e) { + throw new RuntimeException("Add attachments fail: " + e.getMessage(), e); + } finally { + response.close(); + } + } + + public String generateAccessKey(HydraLabAPIConfig apiConfig, String deviceIdentifier) { + checkCenterAlive(apiConfig); + + Request req = new Request.Builder() + .addHeader("Authorization", "Bearer " + apiConfig.authToken) + .url(String.format(apiConfig.getGenerateAccessKeyUrl(), deviceIdentifier)) + .get() + .build(); + OkHttpClient clientToUse = client; + JsonObject jsonObject = null; + Response response = null; + try { + response = clientToUse.newCall(req).execute(); + int waitingRetry = httpFailureRetryTimes; + while (!response.isSuccessful() && waitingRetry > 0) { + printlnf("##[warning]Generate accessKey failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); + response = clientToUse.newCall(req).execute(); + waitingRetry--; + } + + assertTrue(response.isSuccessful(), "generate accessKey", response); + ResponseBody body = response.body(); + + assertNotNull(body, response + ": generateAccessKey ResponseBody"); + jsonObject = GSON.fromJson(body.string(), JsonObject.class); + + int resultCode = jsonObject.get("code").getAsInt(); + assertTrue(resultCode == 200, "Server returned code: " + resultCode, jsonObject); + + return jsonObject.getAsJsonObject("content").get("key").getAsString(); + } catch (Exception e) { + // TODO: no blocking for now, replace after enabling the access key usage + printlnf("##[warning]Request generateAccess failed: " + jsonObject.toString()); + return ""; +// throw new RuntimeException("generate accessKey fail: " + e.getMessage(), e); + } finally { + response.close(); + } + } + + public JsonObject triggerTestRun(String runningType, HydraLabAPIConfig apiConfig, String fileSetId, String testSuiteName, + String deviceIdentifier, @Nullable String accessKey, int runTimeoutSec, Map instrumentationArgs, Map extraArgs) { + checkCenterAlive(apiConfig); + + JsonObject jsonElement = new JsonObject(); + jsonElement.addProperty("runningType", runningType); + jsonElement.addProperty("deviceIdentifier", 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); + + try { + if (apiConfig.neededPermissions.size() > 0) { + jsonElement.add("neededPermissions", GSON.toJsonTree(apiConfig.neededPermissions)); + } + if (StringUtils.isNotBlank(apiConfig.deviceActionsStr)) { + JsonParser parser = new JsonParser(); + JsonObject jsonObject = parser.parse(apiConfig.deviceActionsStr).getAsJsonObject(); + jsonElement.add("deviceActions", jsonObject); + } + if (instrumentationArgs != null) { + jsonElement.add("instrumentationArgs", GSON.toJsonTree(instrumentationArgs).getAsJsonObject()); + } + + } catch (JsonParseException e) { + throw new RuntimeException("trigger test running fail: " + e.getMessage(), e); + } + + 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)); + RequestBody jsonBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), GSON.toJson(jsonElement)); + + Request req = new Request.Builder() + .addHeader("Authorization", "Bearer " + apiConfig.authToken) + .url(apiConfig.getRunTestUrl()) + .post(jsonBody).build(); + OkHttpClient clientToUse = client; + Response response = null; + try { + response = clientToUse.newCall(req).execute(); + int waitingRetry = httpFailureRetryTimes; + while (!response.isSuccessful() && waitingRetry > 0) { + printlnf("##[warning]Trigger test running failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); + response = clientToUse.newCall(req).execute(); + waitingRetry--; + } + + assertTrue(response.isSuccessful(), "trigger test running", response); + ResponseBody body = response.body(); + assertNotNull(body, response + ": triggerTestRun ResponseBody"); + String string = body.string(); + printlnf("RunningTestJson: %s", maskCred(string)); + JsonObject jsonObject = GSON.fromJson(string, JsonObject.class); + + return jsonObject; + } catch (Exception e) { + throw new RuntimeException("trigger test running fail: " + e.getMessage(), e); + } finally { + response.close(); + } + } + + public TestTask getTestStatus(HydraLabAPIConfig apiConfig, String testTaskId) { + checkCenterAlive(apiConfig); + + Request req = new Request.Builder() + .addHeader("Authorization", "Bearer " + apiConfig.authToken) + .url(apiConfig.getTestStatusUrl(testTaskId)) + .build(); + OkHttpClient clientToUse = client; + Response response = null; + try { + response = clientToUse.newCall(req).execute(); + int waitingRetry = httpFailureRetryTimes; + while (!response.isSuccessful() && waitingRetry > 0) { + printlnf("##[warning]Get test status failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); + response = clientToUse.newCall(req).execute(); + waitingRetry--; + } + + assertTrue(response.isSuccessful(), "get test status", response); + ResponseBody body = response.body(); + assertNotNull(body, response + ": getTestStatus ResponseBody"); + JsonObject jsonObject = GSON.fromJson(body.string(), JsonObject.class); + + int resultCode = jsonObject.get("code").getAsInt(); + assertTrue(resultCode == 200, "Server returned code: " + resultCode, jsonObject); + + TestTask result = GSON.fromJson(jsonObject.getAsJsonObject("content"), TestTask.class); + if (result.id == null) { + result.id = testTaskId; + } + return result; + } catch (Exception e) { + throw new RuntimeException("get test status fail: " + e.getMessage(), e); + } finally { + response.close(); + } + } + + public String getBlobSAS(HydraLabAPIConfig apiConfig) { + Request req = new Request.Builder() + .addHeader("Authorization", "Bearer " + apiConfig.authToken) + .url(apiConfig.getBlobSASUrl()) + .build(); + OkHttpClient clientToUse = client; + Response response = null; + try { + response = clientToUse.newCall(req).execute(); + int waitingRetry = httpFailureRetryTimes; + while (!response.isSuccessful() && waitingRetry > 0) { + printlnf("##[warning]Get Blob SAS failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); + response = clientToUse.newCall(req).execute(); + waitingRetry--; + } + + assertTrue(response.isSuccessful(), "Get Blob SAS", response); + ResponseBody body = response.body(); + + assertNotNull(body, response + ": Blob SAS"); + JsonObject jsonObject = GSON.fromJson(body.string(), JsonObject.class); + + int resultCode = jsonObject.get("code").getAsInt(); + assertTrue(resultCode == 200, "Server returned code: " + resultCode, jsonObject); + + return jsonObject.getAsJsonObject("content").get("signature").getAsString(); + } catch (Exception e) { + throw new RuntimeException("Get Blob SAS fail: " + e.getMessage(), e); + } finally { + response.close(); + } + } + + public void downloadToFile(String fileUrl, File file) { + Request req = new Request.Builder().get().url(fileUrl).build(); + try (Response response = client.newCall(req).execute()) { + if (!response.isSuccessful()) { + return; + } + if (response.body() == null) { + return; + } + try (FileOutputStream fos = new FileOutputStream(file)) { + IOUtils.copy(response.body().byteStream(), fos); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void cancelTestTask(HydraLabAPIConfig apiConfig, String testTaskId, String reason) { + checkCenterAlive(apiConfig); + + Request req = new Request.Builder() + .addHeader("Authorization", "Bearer " + apiConfig.authToken) + .url(String.format(apiConfig.getCancelTestTaskUrl(), testTaskId, reason)) + .build(); + OkHttpClient clientToUse = client; + Response response = null; + try { + response = clientToUse.newCall(req).execute(); + int waitingRetry = httpFailureRetryTimes; + while (!response.isSuccessful() && waitingRetry > 0) { + printlnf("##[warning]Cancel test task failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); + response = clientToUse.newCall(req).execute(); + waitingRetry--; + } + + assertTrue(response.isSuccessful(), "cancel test task", response); + } catch (Exception e) { + throw new RuntimeException("cancel test task fail: " + e.getMessage(), e); + } finally { + response.close(); + } + } +} 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 18ee0e89d..082863e1f 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,9 +1,11 @@ package com.microsoft.hydralab.utils; import com.google.gson.*; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -import okhttp3.*; +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 org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; @@ -13,53 +15,23 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -public class HydraLabClientUtils { - private static final OkHttpClient client = new OkHttpClient.Builder() - .readTimeout(300, TimeUnit.SECONDS) - .connectTimeout(300, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build(); +import static com.microsoft.hydralab.utils.CommonUtils.*; +public class HydraLabClientUtils { + private static HydraLabAPIClient hydraLabAPIClient = new HydraLabAPIClient(); private static final int waitStartSec = 30; private static final int minWaitFinishSec = 15; - private static final int httpFailureRetryTimes = 10; - - private static final Gson GSON = new GsonBuilder() - .registerTypeAdapter(Date.class, new TypeAdapter() { - @Override - public void write(JsonWriter out, Date value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.getTime()); - } - } - @Override - public Date read(JsonReader in) throws IOException { - if (in != null) { - try { - return new Date(in.nextLong()); - } catch (IllegalStateException e) { - in.nextNull(); - return null; - } - } else { - return null; - } - } - }).create(); private static boolean isTestRunningFailed = false; private static boolean isTestResultFailed = false; + public static void switchClientInstance(HydraLabAPIClient client) { + hydraLabAPIClient = client; + } + public static void runTestOnDeviceWithApp(String runningType, String appPath, String testAppPath, String attachmentConfigPath, String testSuiteName, @@ -181,7 +153,7 @@ private static void runTestInner(String runningType, String appPath, String test if (!attachmentConfigPath.isEmpty()) { file = new File(attachmentConfigPath); JsonParser parser = new JsonParser(); - attachmentInfos = parser.parse(new FileReader(file)).getAsJsonArray(); + attachmentInfos = parser.parse(new FileReader(file)).getAsJsonArray(); printlnf("Attachment size: %d", attachmentInfos.size()); printlnf("Attachment information: %s", attachmentInfos.toString()); } @@ -191,9 +163,10 @@ private static void runTestInner(String runningType, String appPath, String test } if (apiConfig == null) { - apiConfig = HydraLabAPIConfig.defaultAPI(); + apiConfig = new HydraLabAPIConfig(); } - String testFileSetId = uploadApp(apiConfig, commitId, commitCount, commitMsg, app, testApp); + + String testFileSetId = hydraLabAPIClient.uploadApp(apiConfig, commitId, commitCount, commitMsg, app, testApp); printlnf("##[section]Uploaded test file set id: %s", testFileSetId); assertNotNull(testFileSetId, "testFileSetId"); @@ -201,7 +174,7 @@ private static void runTestInner(String runningType, String appPath, String test 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++){ + for (int index = 0; index < attachmentInfos.size(); index++) { JsonObject attachmentJson = attachmentInfos.get(index).getAsJsonObject(); AttachmentInfo attachmentInfo = GSON.fromJson(attachmentJson, AttachmentInfo.class); @@ -209,13 +182,13 @@ private static void runTestInner(String runningType, String appPath, String test File attachment = new File(attachmentInfo.filePath); assertTrue(attachment.exists(), "Attachment file " + attachmentInfo.fileName + "doesn't exist.", null); - JsonObject responseContent = addAttachment(apiConfig, testFileSetId, attachmentInfo, attachment); + JsonObject responseContent = hydraLabAPIClient.addAttachment(apiConfig, testFileSetId, attachmentInfo, attachment); int resultCode = responseContent.get("code").getAsInt(); int waitingRetry = 10; while (resultCode != 200 && waitingRetry > 0) { printlnf("##[warning]Attachment %s uploading failed, remaining retry times: %d\nServer code: %d, message: %s", attachmentInfo.filePath, waitingRetry, resultCode, responseContent.get("message").getAsString()); - responseContent = addAttachment(apiConfig, testFileSetId, attachmentInfo, attachment); + responseContent = hydraLabAPIClient.addAttachment(apiConfig, testFileSetId, attachmentInfo, attachment); resultCode = responseContent.get("code").getAsInt(); waitingRetry--; } @@ -223,22 +196,21 @@ private static void runTestInner(String runningType, String appPath, String test printlnf("##[command]Attachment %s uploaded successfully", attachmentInfo.filePath); } - String accessKey = generateAccessKey(apiConfig, deviceIdentifier); + String accessKey = hydraLabAPIClient.generateAccessKey(apiConfig, deviceIdentifier); if (StringUtils.isEmpty(accessKey)) { printlnf("##[warning]Access key is empty."); - } - else { + } else { printlnf("##[command]Access key obtained."); } - JsonObject responseContent = triggerTestRun(runningType, apiConfig, testFileSetId, testSuiteName, deviceIdentifier, accessKey, runTimeoutSec, instrumentationArgs, extraArgs); + JsonObject responseContent = hydraLabAPIClient.triggerTestRun(runningType, apiConfig, testFileSetId, testSuiteName, deviceIdentifier, accessKey, runTimeoutSec, instrumentationArgs, extraArgs); 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 = triggerTestRun(runningType, apiConfig, testFileSetId, testSuiteName, deviceIdentifier, accessKey, runTimeoutSec, instrumentationArgs, extraArgs); + responseContent = hydraLabAPIClient.triggerTestRun(runningType, apiConfig, testFileSetId, testSuiteName, deviceIdentifier, accessKey, runTimeoutSec, instrumentationArgs, extraArgs); resultCode = responseContent.get("code").getAsInt(); waitingRetry--; } @@ -258,7 +230,7 @@ private static void runTestInner(String runningType, String appPath, String test while (!finished) { if (TestTask.TestStatus.WAITING.equals(currentStatus)) { if (totalWaitSecond > queueTimeoutSec) { - cancelTestTask(apiConfig, testTaskId, "Queue timeout!"); + hydraLabAPIClient.cancelTestTask(apiConfig, testTaskId, "Queue timeout!"); printlnf("Cancelled the task as timeout %d seconds is reached", queueTimeoutSec); break; } @@ -270,7 +242,7 @@ private static void runTestInner(String runningType, String appPath, String test printlnf("Get test status after running for %d seconds", totalWaitSecond); } - runningTest = getTestStatus(apiConfig, testTaskId); + runningTest = hydraLabAPIClient.getTestStatus(apiConfig, testTaskId); printlnf("Current running test info: %s", runningTest.toString()); assertNotNull(runningTest, "testTask"); @@ -313,7 +285,7 @@ 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); } else if (TestTask.TestStatus.RUNNING.equals(currentStatus)) { - cancelTestTask(apiConfig, testTaskId, "Run timeout!"); + hydraLabAPIClient.cancelTestTask(apiConfig, testTaskId, "Run timeout!"); assertTrue(finished, "Running timeout after waiting for " + runTimeoutSec + " seconds! Test id", runningTest); } @@ -342,10 +314,9 @@ 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)){ + if (StringUtils.isEmpty(tag)) { testFolder = runningType + "-" + utc.format(DateTimeFormatter.ofPattern("MMddHHmmss")); - } - else { + } else { testFolder = runningType + "-" + tag + "-" + utc.format(DateTimeFormatter.ofPattern("MMddHHmmss")); } @@ -375,7 +346,7 @@ private static void runTestInner(String runningType, String appPath, String test String deviceFileFolderPath = file.getAbsolutePath(); if (deviceTestResult.attachments.size() != 0) { - String signature = getBlobSAS(apiConfig); + String signature = hydraLabAPIClient.getBlobSAS(apiConfig); for (BlobFileInfo fileInfo : deviceTestResult.attachments) { String attachmentUrl = fileInfo.blobUrl + "?" + signature; String attachmentFileName = fileInfo.fileName; @@ -383,7 +354,7 @@ private static void runTestInner(String runningType, String appPath, String test printlnf("Start downloading attachment for device %s, device name: %s, file name: %s, link: %s", deviceTestResult.deviceSerialNumber, deviceTestResult.deviceName, attachmentFileName, attachmentUrl); file = new File(deviceFileFolderPath, attachmentFileName); - downloadToFile(attachmentUrl, file); + hydraLabAPIClient.downloadToFile(attachmentUrl, file); printlnf("Finish downloading attachment %s for device %s", attachmentFileName, deviceTestResult.deviceSerialNumber); } @@ -414,7 +385,7 @@ private static void runTestInner(String runningType, String appPath, String test printlnf("##[section]Test task report link:"); printlnf(testReportUrl); printlnf("##vso[task.setvariable variable=TestTaskReportLink;]%s", testReportUrl); - + displayFinalTestState(); } @@ -438,49 +409,13 @@ private static void markTestResultFail() { } private static void displayFinalTestState() { - if (isTestResultFailed){ + if (isTestResultFailed) { printlnf("##[error]Final test state: fail."); - } - else { + } else { printlnf("Final test state: success."); } } - private static void downloadToFile(String fileUrl, File file) { - Request req = new Request.Builder().get().url(fileUrl).build(); - try (Response response = client.newCall(req).execute()) { - if (!response.isSuccessful()) { - return; - } - if (response.body() == null) { - return; - } - try (FileOutputStream fos = new FileOutputStream(file)) { - IOUtils.copy(response.body().byteStream(), fos); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void assertNotNull(Object notnull, String argName) { - if (notnull == null) { - throw new IllegalArgumentException(argName + " is null"); - } - } - - private static void assertTrue(boolean beTrue, String msg, Object data) { - if (!beTrue) { - throw new IllegalStateException(msg + (data == null ? "" : ": " + data)); - } - } - - private static void printlnf(String format, Object... args) { - ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC); - System.out.print("[" + utc.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "] "); - System.out.printf(format + "\n", args); - } - private static void sleepIgnoreInterrupt(int second) { try { Thread.sleep(TimeUnit.SECONDS.toMillis(second)); @@ -489,321 +424,6 @@ private static void sleepIgnoreInterrupt(int second) { } } - private static void checkCenterAlive(HydraLabAPIConfig apiConfig) { - Request req = new Request.Builder() - .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(apiConfig.checkCenterAliveUrl()) - .build(); - OkHttpClient clientToUse = client; - try { - Response response = clientToUse.newCall(req).execute(); - int waitingRetry = httpFailureRetryTimes; - while (!response.isSuccessful() && waitingRetry > 0) { - printlnf("##[warning]Check center alive failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); - response = clientToUse.newCall(req).execute(); - waitingRetry--; - } - - assertTrue(response.isSuccessful(), "check center alive", response); - printlnf("Center is alive, continue on requesting API..."); - } catch (Exception e) { - throw new RuntimeException("check center alive fail: " + e.getMessage(), e); - } - } - - private static String uploadApp(HydraLabAPIConfig apiConfig, String commitId, String commitCount, String commitMsg, File app, File testApp) { - checkCenterAlive(apiConfig); - - MediaType contentType = MediaType.get("application/vnd.android.package-archive"); - MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("commitId", commitId) - .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 (testApp != null) { - multipartBodyBuilder.addFormDataPart("testAppFile", testApp.getName(), RequestBody.create(contentType, testApp)); - } - - Request req = new Request.Builder() - .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(apiConfig.getUploadUrl()) - .post(multipartBodyBuilder.build()) - .build(); - OkHttpClient clientToUse = client; - try { - Response response = clientToUse.newCall(req).execute(); - int waitingRetry = httpFailureRetryTimes; - while (!response.isSuccessful() && waitingRetry > 0) { - printlnf("##[warning]Upload App failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); - response = clientToUse.newCall(req).execute(); - waitingRetry--; - } - - assertTrue(response.isSuccessful(), "upload App", response); - ResponseBody body = response.body(); - - assertNotNull(body, response + ": upload App ResponseBody"); - JsonObject jsonObject = GSON.fromJson(body.string(), JsonObject.class); - - int resultCode = jsonObject.get("code").getAsInt(); - assertTrue(resultCode == 200, "Server returned code: " + resultCode, jsonObject); - - return jsonObject.getAsJsonObject("content").get("id").getAsString(); - } catch (Exception e) { - throw new RuntimeException("upload App fail: " + e.getMessage(), e); - } - } - - private static JsonObject addAttachment(HydraLabAPIConfig apiConfig, String testFileSetId, AttachmentInfo attachmentConfig, File attachment) { - checkCenterAlive(apiConfig); - - // default text file type: text/plain - // default binary file type: application/octet-stream - // todo: check if file is readable, set corresponding type - MediaType contentType = MediaType.get("application/octet-stream"); - MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("fileSetId", testFileSetId) - .addFormDataPart("fileType", attachmentConfig.fileType) - .addFormDataPart("attachment", attachmentConfig.fileName, RequestBody.create(contentType, attachment)); - if (StringUtils.isNotEmpty(attachmentConfig.loadType)) { - multipartBodyBuilder.addFormDataPart("loadType", attachmentConfig.loadType); - } - if (StringUtils.isNotEmpty(attachmentConfig.loadDir)) { - multipartBodyBuilder.addFormDataPart("loadDir", attachmentConfig.loadDir); - } - - Request req = new Request.Builder() - .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(apiConfig.getAddAttachmentUrl()) - .post(multipartBodyBuilder.build()) - .build(); - OkHttpClient clientToUse = client; - try { - Response response = clientToUse.newCall(req).execute(); - int waitingRetry = httpFailureRetryTimes; - while (!response.isSuccessful() && waitingRetry > 0) { - printlnf("##[warning]Add attachments failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); - response = clientToUse.newCall(req).execute(); - waitingRetry--; - } - - assertTrue(response.isSuccessful(), "Add attachments", response); - ResponseBody body = response.body(); - assertNotNull(body, response + ": Add attachments ResponseBody"); - - return GSON.fromJson(body.string(), JsonObject.class); - } catch (Exception e) { - throw new RuntimeException("Add attachments fail: " + e.getMessage(), e); - } - } - - private static String generateAccessKey(HydraLabAPIConfig apiConfig, String deviceIdentifier) { - checkCenterAlive(apiConfig); - - Request req = new Request.Builder() - .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(String.format(apiConfig.getGenerateAccessKeyUrl(), deviceIdentifier)) - .get() - .build(); - OkHttpClient clientToUse = client; - JsonObject jsonObject = null; - try { - Response response = clientToUse.newCall(req).execute(); - int waitingRetry = httpFailureRetryTimes; - while (!response.isSuccessful() && waitingRetry > 0) { - printlnf("##[warning]Generate accessKey failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); - response = clientToUse.newCall(req).execute(); - waitingRetry--; - } - - assertTrue(response.isSuccessful(), "generate accessKey", response); - ResponseBody body = response.body(); - - assertNotNull(body, response + ": generateAccessKey ResponseBody"); - jsonObject = GSON.fromJson(body.string(), JsonObject.class); - - int resultCode = jsonObject.get("code").getAsInt(); - assertTrue(resultCode == 200, "Server returned code: " + resultCode, jsonObject); - - return jsonObject.getAsJsonObject("content").get("key").getAsString(); - } catch (Exception e) { - // TODO: no blocking for now, replace after enabling the access key usage - printlnf("##[warning]Request generateAccess failed: " + jsonObject.toString()); - return ""; -// throw new RuntimeException("generate accessKey fail: " + e.getMessage(), e); - } - } - - private static JsonObject triggerTestRun(String runningType, HydraLabAPIConfig apiConfig, String fileSetId, String testSuiteName, - String deviceIdentifier, @Nullable String accessKey, int runTimeoutSec, Map instrumentationArgs, Map extraArgs) { - checkCenterAlive(apiConfig); - - JsonObject jsonElement = new JsonObject(); - jsonElement.addProperty("runningType", runningType); - jsonElement.addProperty("deviceIdentifier", 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); - - if (accessKey != null) { - jsonElement.addProperty("accessKey", accessKey); - } - if (instrumentationArgs != null) { - jsonElement.add("instrumentationArgs", GSON.toJsonTree(instrumentationArgs).getAsJsonObject()); - } - if (extraArgs != null) { - extraArgs.forEach(jsonElement::addProperty); - } - - String content = GSON.toJson(jsonElement); - printlnf("triggerTestRun api post body: %s", maskCred(content)); - RequestBody jsonBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), GSON.toJson(jsonElement)); - - Request req = new Request.Builder() - .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(apiConfig.getRunTestUrl()) - .post(jsonBody).build(); - OkHttpClient clientToUse = client; - try { - Response response = clientToUse.newCall(req).execute(); - int waitingRetry = httpFailureRetryTimes; - while (!response.isSuccessful() && waitingRetry > 0) { - printlnf("##[warning]Trigger test running failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); - response = clientToUse.newCall(req).execute(); - waitingRetry--; - } - - assertTrue(response.isSuccessful(), "trigger test running", response); - ResponseBody body = response.body(); - assertNotNull(body, response + ": triggerTestRun ResponseBody"); - String string = body.string(); - printlnf("RunningTestJson: %s", maskCred(string)); - JsonObject jsonObject = GSON.fromJson(string, JsonObject.class); - - return jsonObject; - } catch (Exception e) { - throw new RuntimeException("trigger test running fail: " + e.getMessage(), e); - } - } - - private static TestTask getTestStatus(HydraLabAPIConfig apiConfig, String testTaskId) { - checkCenterAlive(apiConfig); - - Request req = new Request.Builder() - .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(apiConfig.getTestStatusUrl(testTaskId)) - .build(); - OkHttpClient clientToUse = client; - try { - Response response = clientToUse.newCall(req).execute(); - int waitingRetry = httpFailureRetryTimes; - while (!response.isSuccessful() && waitingRetry > 0) { - printlnf("##[warning]Get test status failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); - response = clientToUse.newCall(req).execute(); - waitingRetry--; - } - - assertTrue(response.isSuccessful(), "get test status", response); - ResponseBody body = response.body(); - assertNotNull(body, response + ": getTestStatus ResponseBody"); - JsonObject jsonObject = GSON.fromJson(body.string(), JsonObject.class); - - int resultCode = jsonObject.get("code").getAsInt(); - assertTrue(resultCode == 200, "Server returned code: " + resultCode, jsonObject); - - TestTask result = GSON.fromJson(jsonObject.getAsJsonObject("content"), TestTask.class); - if (result.id == null) { - result.id = testTaskId; - } - return result; - } catch (Exception e) { - throw new RuntimeException("get test status fail: " + e.getMessage(), e); - } - } - - private static String getBlobSAS(HydraLabAPIConfig apiConfig) { - Request req = new Request.Builder() - .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(apiConfig.getBlobSASUrl()) - .build(); - OkHttpClient clientToUse = client; - try { - Response response = clientToUse.newCall(req).execute(); - int waitingRetry = httpFailureRetryTimes; - while (!response.isSuccessful() && waitingRetry > 0) { - printlnf("##[warning]Get Blob SAS failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); - response = clientToUse.newCall(req).execute(); - waitingRetry--; - } - - assertTrue(response.isSuccessful(), "Get Blob SAS", response); - ResponseBody body = response.body(); - - assertNotNull(body, response + ": Blob SAS"); - JsonObject jsonObject = GSON.fromJson(body.string(), JsonObject.class); - - int resultCode = jsonObject.get("code").getAsInt(); - assertTrue(resultCode == 200, "Server returned code: " + resultCode, jsonObject); - - return jsonObject.getAsJsonObject("content").get("signature").getAsString(); - } catch (Exception e) { - throw new RuntimeException("Get Blob SAS fail: " + e.getMessage(), e); - } - } - - private static void cancelTestTask(HydraLabAPIConfig apiConfig, String testTaskId, String reason) { - checkCenterAlive(apiConfig); - - Request req = new Request.Builder() - .addHeader("Authorization", "Bearer " + apiConfig.authToken) - .url(String.format(apiConfig.getCancelTestTaskUrl(), testTaskId, reason)) - .build(); - OkHttpClient clientToUse = client; - try { - Response response = clientToUse.newCall(req).execute(); - int waitingRetry = httpFailureRetryTimes; - while (!response.isSuccessful() && waitingRetry > 0) { - printlnf("##[warning]Cancel test task failed, remaining retry times: %d\nHttp code: %d\nHttp message: %s", waitingRetry, response.code(), response.message()); - response = clientToUse.newCall(req).execute(); - waitingRetry--; - } - - assertTrue(response.isSuccessful(), "cancel test task", response); - } catch (Exception e) { - throw new RuntimeException("cancel test task fail: " + e.getMessage(), e); - } - } - - private static String maskCred(String content) { - for (MaskSensitiveData sensitiveData : MaskSensitiveData.values()) { - Pattern PATTERNCARD = Pattern.compile(sensitiveData.getRegEx(), Pattern.CASE_INSENSITIVE); - Matcher matcher = PATTERNCARD.matcher(content); - if (matcher.find()) { - String maskedMessage = matcher.group(2); - if (maskedMessage.length() > 0) { - content = content.replaceFirst(maskedMessage, "***"); - } - } - } - - return content; - } public static String getCommitCount(File commandDir, String startCommit) throws IOException { Process process = Runtime.getRuntime().exec(String.format("git rev-list --first-parent --right-only --count %s..HEAD", startCommit), null, commandDir.getAbsoluteFile()); @@ -832,198 +452,6 @@ public static String getCommitMessage(File workingDirFile, String commitId) thro } } - public static 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"; - public String addAttachmentAPIPath = "/api/package/addAttachment"; - public String generateAccessKeyAPIPath = "/api/deviceGroup/generate?deviceIdentifier=%s"; - public String runTestAPIPath = "/api/test/task/run/"; - public String testStatusAPIPath = "/api/test/task/"; - 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 static HydraLabAPIConfig defaultAPI() { - return new HydraLabAPIConfig(); - } - - public String getBlobSASUrl() { - return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, getBlobSAS); - } - - public String checkCenterAliveUrl() { - return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, checkCenterAliveAPIPath); - } - - public String getUploadUrl() { - return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, uploadAPKAPIPath); - } - - public String getAddAttachmentUrl() { - return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, addAttachmentAPIPath); - } - - public String getGenerateAccessKeyUrl() { - return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, generateAccessKeyAPIPath); - } - - public String getRunTestUrl() { - return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, runTestAPIPath); - } - - public String getTestStatusUrl(String testTaskId) { - return String.format(Locale.US, "%s://%s%s%s%s", schema, host, contextPath, testStatusAPIPath, testTaskId); - } - - public String getCancelTestTaskUrl() { - return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, cancelTestTaskAPIPath); - } - - public String getTestReportUrl(String testTaskId) { - return String.format(Locale.US, "%s://%s%s%s%s", schema, host, contextPath, testPortalTaskInfoPath, testTaskId); - } - - public String getDeviceTestVideoUrl(String id) { - return String.format(Locale.US, "%s://%s%s%s%s", schema, host, contextPath, testPortalTaskDeviceVideoPath, id); - } - - @Override - public String toString() { - return "HydraLabAPIConfig Upload URL {" + getUploadUrl() + '}'; - } - - public String getTestStaticResUrl(String resPath) { - return String.format(Locale.US, "%s://%s%s%s", schema, host, contextPath, resPath); - } - } - - public static class AttachmentInfo { - String fileName; - String filePath; - String fileType; - String loadType; - String loadDir; - } - - public static class TestTask { - public String id; - public List deviceTestResults; - public int testDevicesCount; - public Date startDate; - public Date endDate; - public int totalTestCount; - public int totalFailCount; - public String testSuite; - public String reportImagePath; - public String baseUrl; - public String status; - public String testErrorMsg; - public String message; - public int retryTime; - - @Override - public String toString() { - return "TestTask{" + - "id='" + id + '\'' + - ", testDevicesCount=" + testDevicesCount + - ", startDate=" + startDate + - ", totalTestCount=" + totalTestCount + - ", status='" + status + '\'' + - '}'; - } - - public interface TestStatus { - String RUNNING = "running"; - String FINISHED = "finished"; - String CANCELED = "canceled"; - String EXCEPTION = "error"; - String WAITING = "waiting"; - } - } - - public static class DeviceTestResult { - public String id; - public String deviceSerialNumber; - public String deviceName; - public String instrumentReportPath; - public String controlLogPath; - public String instrumentReportBlobUrl; - public String testXmlReportBlobUrl; - public String logcatBlobUrl; - public String testGifBlobUrl; - - public List attachments; - - public String crashStackId; - public String errorInProcess; - - public String crashStack; - - public int totalCount; - public int failCount; - public boolean success; - public long testStartTimeMillis; - public long testEndTimeMillis; - - @Override - public String toString() { - return "{" + - "SN='" + deviceSerialNumber + '\'' + - ", totalCase:" + totalCount + - ", failCase:" + failCount + - ", success:" + success + - '}'; - } - } - - public static class BlobFileInfo { - public String fileId; - public String fileType; - public String fileName; - public String blobUrl; - public String blobPath; - public long fileLen; - public String md5; - public String loadDir; - public String loadType; - public JsonObject fileParser; - public Date createTime; - public Date updateTime; - - - public interface fileType { - String WINDOWS_APP = "WINAPP"; - String COMMOM_FILE = "COMMON"; - String AGENT_PACKAGE = "PACKAGE"; - String APP_FILE = "APP"; - String TEST_APP_FILE = "TEST_APP"; - } - - public interface loadType { - String CPOY = "COPY"; - String UNZIP = "UNZIP"; - } - } - public enum MaskSensitiveData { CURRENT_PASSWORD("(current[_\\s-]*password)[=:\"\\s]*(\\w*)"), PASSWORD("[&,;\"\'\\s]+(password|pwd)[=:\"\\s]*(\\w*)"), diff --git a/gradle_plugin/src/main/resources/template/gradle.properties b/gradle_plugin/src/main/resources/template/gradle.properties index a73793b20..eacc5522d 100644 --- a/gradle_plugin/src/main/resources/template/gradle.properties +++ b/gradle_plugin/src/main/resources/template/gradle.properties @@ -4,11 +4,13 @@ pkgName = # Required, absolute package name of the app. deviceIdentifier = # Required, identifier of the device / group of devices for running the test. For APPIUM_CROSS test specifically, the agentId for agent. Please obtain the value from the front page. 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;; # 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} 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: # [{ # "fileName":"name of file to be uploaded", @@ -21,6 +23,16 @@ instrumentationArgs = # Optional, All extra params. Example: "a1=x1|x2,b1=x3|x4| # ... #}] 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. +# 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. +# See more details in Hydra Lab wiki (section: TBD). +# Example: "{\"setUp\":[{\"method\":\"setProperty\",\"args\":[\"value A\", \"value B\"]}, {...}, {...}], \"tearDown\":[{\"method\":\"backToHome\",\"args\":[]}, {...}, {...}]}" +deviceActions = + # 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) @@ -39,6 +51,7 @@ teamName = # Required for test type: INSTRUMENTATION, APPIUM, APPIUM_CROSS testAppPath = # Path to the test app/jar or JSON-described test file. + # Optional for test type: INSTRUMENTATION # Define test scope of the given test package. # Value: {TEST_APP (Default if both testScope and testSuiteName are empty), PACKAGE, CLASS (Default)} @@ -56,17 +69,21 @@ testSuiteName = # Required for test type: INSTRUMENTATION testPkgName = # Absolute package name of the test app. + # Optional for test type: SMART, APPIUM_MONKEY maxStepCount = # The max step count for each SMART test. # Optional for test type: SMART deviceTestCount = # The number of times to run SMART test. + # Optional for test type: INSTRUMENTATION testRunnerName = # The testInstrumentationRunner of INSTRUMENTATION (Espresso) test. Value: {androidx.test.runner.AndroidJUnitRunner (Default), customized test runner name} + # Optional for test type: APPIUM, APPIUM_CROSS frameworkType = # JUNIT version used for Appium test. Value: {JUNIT4 (Default), JUNIT5} + # Optional for test type: APPIUM_CROSS, T2C_JSON needUninstall = # Apk needs to be uninstalled. Value: {true (Default), false} needClearData = # Apk data needs to be cleaned. Value: {true (Default), false} \ 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 new file mode 100644 index 000000000..4fdfd27ec --- /dev/null +++ b/gradle_plugin/src/test/java/com/microsoft/hydralab/ClientUtilsPluginTest.java @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +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.utils.HydraLabAPIClient; +import com.microsoft.hydralab.utils.HydraLabClientUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.*; + + +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 = ""; + apiConfig.authToken = ""; + String testAppPath = "./testAppPath/testApp.apk"; + String testSuiteName = "com.example.test.suite"; + apiConfig.testPkgName = "TestPkgName"; + apiConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; + + generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + + runningType = "INSTRUMENTATION"; + generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + + appPath = "./appPath/app.apk"; + generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + + deviceIdentifier = "TESTDEVICESN001"; + generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + + runTimeOutSeconds = "1000"; + generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + + apiConfig.pkgName = "PkgName"; + generalParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType); + + apiConfig.authToken = "thisisanauthtokenonlyfortest"; + clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + } + + @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"; + apiConfig.authToken = "thisisanauthtokenonlyfortest"; + String testAppPath = ""; + String testSuiteName = ""; + apiConfig.testPkgName = ""; + apiConfig.testScope = ""; + + typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testAppPath"); + testAppPath = "./testAppPath/testApp.apk"; + + typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testPkgName"); + apiConfig.testPkgName = "TestPkgName"; + + apiConfig.testScope = ClientUtilsPlugin.TestScope.TEST_APP; + clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + + apiConfig.testScope = ClientUtilsPlugin.TestScope.PACKAGE; + typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testSuiteName"); + + apiConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; + typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testSuiteName"); + + apiConfig.testScope = ClientUtilsPlugin.TestScope.PACKAGE; + testSuiteName = "com.example.test.suite"; + clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + + apiConfig.testScope = ClientUtilsPlugin.TestScope.CLASS; + clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + } + + @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"; + apiConfig.authToken = "thisisanauthtokenonlyfortest"; + String testAppPath = ""; + String testSuiteName = ""; + + typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testAppPath"); + testAppPath = "./testAppPath/testApp.apk"; + + typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testSuiteName"); + testSuiteName = "com.example.test.suite"; + + clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + } + + @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"; + apiConfig.authToken = "thisisanauthtokenonlyfortest"; + String testAppPath = ""; + String testSuiteName = ""; + + typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testAppPath"); + testAppPath = "./testAppPath/testApp.apk"; + + typeSpecificParamCheck(appPath, deviceIdentifier, runTimeOutSeconds, apiConfig, testAppPath, testSuiteName, runningType, "testSuiteName"); + testSuiteName = "com.example.test.suite"; + + clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + } + + @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); + + String returnId = "id123456"; + when(client.uploadApp(Mockito.any(HydraLabAPIConfig.class), Mockito.anyString(), Mockito.anyString(), + Mockito.anyString(), Mockito.any(File.class), Mockito.any(File.class))) + .thenReturn(returnId); + + JsonObject returnJson = new JsonObject(); + returnJson.addProperty("code", "200"); + returnJson.addProperty("message", "OK!"); + when(client.addAttachment(Mockito.any(HydraLabAPIConfig.class), Mockito.anyString(), + Mockito.any(AttachmentInfo.class), Mockito.any(File.class))) + .thenReturn(returnJson); + + when(client.generateAccessKey(Mockito.any(HydraLabAPIConfig.class), Mockito.anyString())) + .thenReturn("accessKey"); + + returnJson = new JsonObject(); + returnJson.addProperty("code", "200"); + returnJson.addProperty("message", "OK!"); + JsonObject subJsonObject = new JsonObject(); + 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())) + .thenReturn(returnJson); + + TestTask returnTestTask = new TestTask(); + returnTestTask.status = TestTask.TestStatus.FINISHED; + returnTestTask.retryTime = 1; + returnTestTask.deviceTestResults = new ArrayList<>(); + returnTestTask.message = "message"; + returnTestTask.id = "id"; + returnTestTask.testDevicesCount = 1; + returnTestTask.totalTestCount = 5; + returnTestTask.totalFailCount = 1; + returnTestTask.reportImagePath = "./image_path/image"; + when(client.getTestStatus(Mockito.any(HydraLabAPIConfig.class), Mockito.anyString())) + .thenReturn(returnTestTask); + + String returnBlobSAS = "SAS"; + when(client.getBlobSAS(Mockito.any(HydraLabAPIConfig.class))) + .thenReturn(returnBlobSAS); + + HydraLabClientUtils.switchClientInstance(client); + HydraLabClientUtils.runTestOnDeviceWithApp(runningType, appPath, testAppPath, attachmentConfigPath, + testSuiteName, deviceIdentifier, queueTimeoutSec, runTimeoutSec, reportFolderPath, instrumentationArgs, + extraArgs, tag, apiConfig); + + verify(client, times(0)).cancelTestTask(Mockito.any(HydraLabAPIConfig.class), Mockito.anyString(), Mockito.anyString()); + verify(client, times(0)).downloadToFile(Mockito.anyString(), Mockito.any(File.class)); + } + + @Test + public void getLatestCommitInfo() { + String commitId = null; + String commitCount = null; + String commitMsg = null; + File commandDir = new File("."); + try { + commitId = HydraLabClientUtils.getLatestCommitHash(commandDir); + commitCount = HydraLabClientUtils.getCommitCount(commandDir, commitId); + commitMsg = HydraLabClientUtils.getCommitMessage(commandDir, commitId); + } catch (IOException e) { + e.printStackTrace(); + } + + Assertions.assertNotNull(commitId, "Get commit id error"); + Assertions.assertNotNull(commitCount, "Get commit count error"); + Assertions.assertNotNull(commitMsg, "Get commit message error"); + } + + private void generalParamCheck(String appPath, String deviceIdentifier, String runTimeOutSeconds, HydraLabAPIConfig apiConfig, String testAppPath, String testSuiteName, String runningType) { + IllegalArgumentException thrown = Assertions.assertThrows(IllegalArgumentException.class, () -> { + clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + }, "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()); + } + + private void typeSpecificParamCheck(String appPath, String deviceIdentifier, String runTimeOutSeconds, HydraLabAPIConfig apiConfig, String testAppPath, String testSuiteName, String runningType, String requiredParamName) { + IllegalArgumentException thrown = Assertions.assertThrows(IllegalArgumentException.class, () -> { + clientUtilsPlugin.requiredParamCheck(runningType, appPath, testAppPath, deviceIdentifier, runTimeOutSeconds, testSuiteName, apiConfig); + }, "IllegalArgumentException was expected"); + Assertions.assertEquals("Required param " + requiredParamName + " not provided!", thrown.getMessage()); + } +} \ No newline at end of file diff --git a/gradle_plugin/src/test/java/com/microsoft/hydralab/utils/HydraLabClientUtilsTest.java b/gradle_plugin/src/test/java/com/microsoft/hydralab/utils/HydraLabClientUtilsTest.java deleted file mode 100644 index f3d7eee9e..000000000 --- a/gradle_plugin/src/test/java/com/microsoft/hydralab/utils/HydraLabClientUtilsTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.microsoft.hydralab.utils; - - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.io.IOException; - -public class HydraLabClientUtilsTest { - - @Test - public void runTestOnDeviceWithApp() { - } - - @Test - public void getLatestCommitInfo() { - String commitId = null; - String commitCount = null; - String commitMsg = null; - File commandDir = new File("."); - try { - commitId = HydraLabClientUtils.getLatestCommitHash(commandDir); - commitCount = HydraLabClientUtils.getCommitCount(commandDir, commitId); - commitMsg = HydraLabClientUtils.getCommitMessage(commandDir, commitId); - } catch (IOException e) { - e.printStackTrace(); - } - - Assertions.assertNotNull(commitId, "Get commit id error"); - Assertions.assertNotNull(commitCount, "Get commit count error"); - Assertions.assertNotNull(commitMsg, "Get commit message error"); - } -} \ No newline at end of file diff --git a/gradle_plugin/src/test/resources/app.txt b/gradle_plugin/src/test/resources/app.txt new file mode 100644 index 000000000..e69de29bb diff --git a/gradle_plugin/src/test/resources/test_app.txt b/gradle_plugin/src/test/resources/test_app.txt new file mode 100644 index 000000000..e69de29bb