Skip to content

Commit

Permalink
Test orchestrator support (#634)
Browse files Browse the repository at this point in the history
<!-- Please provide brief information about the PR, what it contains &
its purpose, new behaviors after the change. And let us know here if you
need any help: https://github.com/microsoft/HydraLab/issues/new -->
Update backend/frontend/gradle plugin for Test Orchestrator

## Description

<!-- A few words to explain your changes -->

### Linked GitHub issue ID: #  

## Pull Request Checklist
<!-- Put an x in the boxes that apply. This is simply a reminder of what
we are going to look for before merging your code. -->

- [X] Tests for the changes have been added (for bug fixes / features)
- [X] Code compiles correctly with all tests are passed.
- [ ] I've read the [contributing
guide](https://github.com/microsoft/HydraLab/blob/main/CONTRIBUTING.md#making-changes-to-the-code)
and followed the recommended practices.
- [ ] [Wikis](https://github.com/microsoft/HydraLab/wiki) or
[README](https://github.com/microsoft/HydraLab/blob/main/README.md) have
been reviewed and added / updated if needed (for bug fixes / features)

### Does this introduce a breaking change?
*If this introduces a breaking change for Hydra Lab users, please
describe the impact and migration path.*

- [ ] Yes
- [X] No

## How you tested it
*Please make sure the change is tested, you can test it by adding UTs,
do local test and share the screenshots, etc.*


Please check the type of change your PR introduces:
- [ ] Bugfix
- [X] Feature
- [ ] Technical design
- [ ] Build related changes
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Code style update (formatting, renaming) or Documentation content
changes
- [ ] Other (please describe): 

### Feature UI screenshots or Technical design diagrams
*If this is a relatively large or complex change, kick it off by drawing
the tech design with PlantUML and explaining why you chose the solution
you did and what alternatives you considered, etc...*
  • Loading branch information
olivershen-wow committed Jan 3, 2024
1 parent 76f4dff commit d931f84
Show file tree
Hide file tree
Showing 18 changed files with 147 additions and 19 deletions.
4 changes: 3 additions & 1 deletion agent/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
storage
log
log
!/src/main/resources/test-services.apk
!/src/main/resources/orchestrator.apk
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Map;

Expand Down Expand Up @@ -134,6 +135,7 @@ public AgentManagementService agentManagementService(StorageServiceClientProxy s

agentManagementService.setEnvInfo(envCapabilityDiscoveryService.getEnvInfo());
agentManagementService.setRegistryServer(registryServer);
agentManagementService.setTestTempFilePath(Path.of(appOptions.getTestPackageLocation(), "temp").toString());

logger.info("done with agentManagementService instantiation");
return agentManagementService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
import com.microsoft.hydralab.common.entity.common.TestRun;
import com.microsoft.hydralab.common.entity.common.TestRunDevice;
import com.microsoft.hydralab.common.entity.common.TestTask;
import com.microsoft.hydralab.common.logger.MultiLineNoCancelReceiver;
import com.microsoft.hydralab.common.management.AgentManagementService;
import com.microsoft.hydralab.common.util.ADBOperateUtil;
import com.microsoft.hydralab.common.util.Const;
import com.microsoft.hydralab.common.util.FlowUtil;
import com.microsoft.hydralab.common.util.LogUtils;
import com.microsoft.hydralab.common.util.ThreadUtils;
import com.microsoft.hydralab.performance.PerformanceTestManagementService;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
Expand All @@ -28,6 +31,9 @@
import java.util.List;
import java.util.Map;

import static com.microsoft.hydralab.common.util.AgentConstant.ESPRESSO_TEST_ORCHESTRATOR_APK;
import static com.microsoft.hydralab.common.util.AgentConstant.ESPRESSO_TEST_SERVICES_APK;

public class EspressoRunner extends TestRunner {
private static final int MAJOR_ADB_VERSION = 1;
private static final int MINOR_ADB_VERSION = -1;
Expand All @@ -45,6 +51,14 @@ protected List<EnvCapabilityRequirement> getEnvCapabilityRequirements() {
return List.of(new EnvCapabilityRequirement(EnvCapability.CapabilityKeyword.adb, MAJOR_ADB_VERSION, MINOR_ADB_VERSION));
}

@Override
protected void setUp(TestRunDevice testRunDevice, TestTask testTask, TestRun testRun) throws Exception {
super.setUp(testRunDevice, testTask, testRun);
if (testTask.isEnableTestOrchestrator()) {
reinstallOrchestratorDependency(testRunDevice, testTask, testRun.getLogger());
}
}

@Override
protected void run(TestRunDevice testRunDevice, TestTask testTask, TestRun testRun) throws Exception {
InstrumentationResultParser instrumentationResultParser = null;
Expand All @@ -69,8 +83,21 @@ public boolean isCancelled() {
reportLogger.info("Start instrumenting the test");
checkTestTaskCancel(testTask);
listener.startRecording(testTask.getTimeOutSecond());

StringBuilder pathToTestServicePack = new StringBuilder();
if (testTask.isEnableTestOrchestrator()) {
adbOperateUtil.executeShellCommandOnDevice(testRunDevice.getDeviceInfo(), "pm path androidx.test.services", new MultiLineNoCancelReceiver() {
@Override
public void processNewLines(@NotNull String[] lines) {
for (String line : lines) {
pathToTestServicePack.append(line);
}
}
}, testTask.getTimeOutSecond(), -1);
}

String command = buildCommand(testTask.getTestSuite(), testTask.getTestPkgName(), testTask.getTestRunnerName(),
testTask.getTestScope(), testTask.getInstrumentationArgs());
testTask.getTestScope(), testTask.getInstrumentationArgs(), testTask.isEnableTestOrchestrator(), pathToTestServicePack.toString());
String result = startInstrument(testRunDevice.getDeviceInfo(), reportLogger,
instrumentationResultParser, testTask.getTimeOutSecond(), command);
if (Const.TaskResult.ERROR_DEVICE_OFFLINE.equals(result)) {
Expand Down Expand Up @@ -128,7 +155,7 @@ public String startInstrument(DeviceInfo deviceInfo, Logger logger, IShellOutput

@NotNull
private String buildCommand(String suiteName, String testPkgName,
String testRunnerName, String scope, Map<String, String> instrumentationArgs) {
String testRunnerName, String scope, Map<String, String> instrumentationArgs, boolean enableTestOrchestrator, String pathToTestServicePack) {
StringBuilder argString = new StringBuilder();
if (instrumentationArgs != null && !instrumentationArgs.isEmpty()) {
instrumentationArgs.forEach(
Expand All @@ -143,21 +170,44 @@ private String buildCommand(String suiteName, String testPkgName,
}
String command;
switch (scope) {
case TestTask.TestScope.TEST_APP:
commFormat += " %s/%s";
command = String.format(commFormat, testPkgName, testRunnerName);
break;
case TestTask.TestScope.PACKAGE:
commFormat += " -e package %s %s/%s";
command = String.format(commFormat, suiteName, testPkgName, testRunnerName);
commFormat += " -e package %s";
commFormat = String.format(commFormat, suiteName);
break;
case TestTask.TestScope.CLASS:
commFormat += " -e class %s";
commFormat = String.format(commFormat, suiteName);
break;
// Const.TestScope.CLASS
default:
commFormat += " -e class %s %s/%s";
command = String.format(commFormat, suiteName, testPkgName, testRunnerName);
break;
}
if (enableTestOrchestrator) {
commFormat = "CLASSPATH='" + pathToTestServicePack + "' app_process / androidx.test.services.shellexecutor.ShellMain " + commFormat +
" -e targetInstrumentation %s/%s androidx.test.orchestrator/.AndroidTestOrchestrator";
} else {
commFormat += " %s/%s";
}
command = String.format(commFormat, testPkgName, testRunnerName);

return command;
}

protected void reinstallOrchestratorDependency(TestRunDevice testRunDevice, TestTask testTask, Logger reportLogger) throws Exception {
checkTestTaskCancel(testTask);

String pathToTestOrchestratorApk = this.agentManagementService.copyPreinstallAPK(ESPRESSO_TEST_ORCHESTRATOR_APK);
String pathToTestServicesApk = this.agentManagementService.copyPreinstallAPK(ESPRESSO_TEST_SERVICES_APK);

testRunDeviceOrchestrator.uninstallApp(testRunDevice, "androidx.test.services", reportLogger);
ThreadUtils.safeSleep(2000);
testRunDeviceOrchestrator.uninstallApp(testRunDevice, "androidx.test.orchestrator", reportLogger);
ThreadUtils.safeSleep(2000);

FlowUtil.retryAndSleepWhenFalse(3, 5,
() -> testRunDeviceOrchestrator.installApp(testRunDevice, new File(pathToTestOrchestratorApk).getAbsolutePath(),
reportLogger));
FlowUtil.retryAndSleepWhenFalse(3, 5,
() -> testRunDeviceOrchestrator.installApp(testRunDevice, new File(pathToTestServicesApk).getAbsolutePath(),
reportLogger));
}
}
Binary file added agent/src/main/resources/orchestrator.apk
Binary file not shown.
Binary file added agent/src/main/resources/test-services.apk
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ public class TestTask implements Serializable {
private boolean enableNetworkMonitor;
@Transient
private String networkMonitorRule;
@Column(nullable = true)
private boolean enableTestOrchestrator = false;

public TestTask() {
}
Expand Down Expand Up @@ -182,6 +184,7 @@ public static TestTask convertToTestTask(TestTaskSpec testTaskSpec) {
testTask.setDisableRecording(testTaskSpec.disableRecording);
testTask.setEnableNetworkMonitor(testTaskSpec.enableNetworkMonitor);
testTask.setNetworkMonitorRule(testTaskSpec.networkMonitorRule);
testTask.setEnableTestOrchestrator(testTaskSpec.enableTestOrchestrator);

return testTask;
}
Expand Down Expand Up @@ -224,6 +227,7 @@ public static TestTaskSpec convertToTestTaskSpec(TestTask testTask) {
testTaskSpec.enableNetworkMonitor = testTask.isEnableNetworkMonitor();
testTaskSpec.networkMonitorRule = testTask.getNetworkMonitorRule();
testTaskSpec.retryTime = testTask.getRetryTime();
testTaskSpec.enableTestOrchestrator = testTask.isEnableTestOrchestrator();

return testTaskSpec;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class TestTaskSpec {
public boolean disableRecording = false;
public boolean enableNetworkMonitor;
public String networkMonitorRule;
public boolean enableTestOrchestrator = false;

public void updateWithDefaultValues() {
determineScopeOfTestCase();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@
import com.microsoft.hydralab.common.file.StorageServiceClientProxy;
import com.microsoft.hydralab.common.management.listener.DeviceStatusListener;
import com.microsoft.hydralab.common.management.listener.DeviceStatusListenerManager;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -45,6 +52,7 @@ public class AgentManagementService {
protected String preInstallFailurePolicy;
protected EnvInfo envInfo;
private List<AgentFunctionAvailability> functionAvailabilities = new ArrayList<>();
private String testTempFilePath;

public List<AgentFunctionAvailability> getFunctionAvailabilities() {
return functionAvailabilities;
Expand Down Expand Up @@ -238,4 +246,28 @@ public void registerFunctionAvailability(String functionName, AgentFunctionAvail
}
functionAvailabilities.add(new AgentFunctionAvailability(functionName, functionType, enabled, available, requirements));
}

public void setTestTempFilePath(String tempFilePath) {
File dir = new File(tempFilePath);
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new RuntimeException("create dir fail: " + dir.getAbsolutePath());
}
}
this.testTempFilePath = tempFilePath;
}

public String copyPreinstallAPK(String fileName) {
File preinstallApk = new File(this.testTempFilePath, fileName);
if (preinstallApk.exists()) {
preinstallApk.delete();
}
try (InputStream resourceAsStream = FileUtils.class.getClassLoader().getResourceAsStream(fileName); OutputStream out = new FileOutputStream(preinstallApk)) {
IOUtils.copy(Objects.requireNonNull(resourceAsStream), out);
} catch (IOException e) {
e.printStackTrace();
}

return preinstallApk.getAbsolutePath();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,7 @@ public class AgentConstant {
put("Watch6,8", "Apple Watch Series 7 41mm case (GPS+Cellular)");
put("Watch6,9", "Apple Watch Series 7 45mm case (GPS+Cellular)");
}};

public static final String ESPRESSO_TEST_ORCHESTRATOR_APK = "orchestrator.apk";
public static final String ESPRESSO_TEST_SERVICES_APK = "test-services.apk";
}
2 changes: 1 addition & 1 deletion gradle_plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dependencies {
}

// plugin publishing related
version = '1.1.13'
version = '1.1.14'
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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,6 @@ testSpec:
enableFailingTask: true
enableNetworkMonitor: false
networkMonitorRule: NETWORK_TEST_RULE_NONE
enableTestOrchestrator: false

@endyaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ class ClientUtilsPlugin implements Plugin<Project> {
if (project.hasProperty('networkMonitorRule')) {
testConfig.networkMonitorRule = project.networkMonitorRule
}
if (project.hasProperty('enableTestOrchestrator')) {
testConfig.enableTestOrchestrator = project.enableTestOrchestrator
}

requiredParamCheck(apiConfig, testConfig)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package com.microsoft.hydralab.config;

import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down Expand Up @@ -55,13 +56,14 @@ public class TestConfig {
public boolean enableFailingTask = true;
public boolean enableNetworkMonitor = false;
public String networkMonitorRule = "";
public boolean enableTestOrchestrator = false;

public void constructField(HashMap<String, Object> map) {
Object queueTimeOutSeconds = map.get("queueTimeOutSeconds");
if (queueTimeOutSeconds == null) {
this.queueTimeOutSeconds = this.runTimeOutSeconds;
}
HashMap<String, Object> explorationArgs = (HashMap<String, Object>)map.get("exploration");
HashMap<String, Object> explorationArgs = (HashMap<String, Object>) map.get("exploration");
if (explorationArgs != null) {
Object maxStepCount = explorationArgs.get("maxStepCount");
if (maxStepCount != null) {
Expand All @@ -74,7 +76,7 @@ public void constructField(HashMap<String, Object> map) {
}
}

public void extractFromExistingField(){
public void extractFromExistingField() {
if (StringUtils.isBlank(this.inspectionStrategiesStr) && this.inspectionStrategies.size() != 0) {
this.inspectionStrategiesStr = GSON.toJson(this.inspectionStrategies);
}
Expand Down Expand Up @@ -113,7 +115,8 @@ public String toString() {
"\tnotifyUrl=" + notifyUrl + "\n" +
"\tdisableRecording=" + disableRecording + "\n" +
"\tenableFailingTask=" + enableFailingTask + "\n" +
"\tenableNetworkMonitor=" + enableNetworkMonitor +"\n" +
"\tnetworkMonitorRule=" + networkMonitorRule;
"\tenableNetworkMonitor=" + enableNetworkMonitor + "\n" +
"\tnetworkMonitorRule=" + networkMonitorRule + "\n" +
"\tenableTestOrchestrator=" + enableTestOrchestrator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ public JsonObject triggerTestRun(TestConfig testConfig, HydraLabAPIConfig apiCon
jsonElement.addProperty("disableRecording", testConfig.disableRecording);
jsonElement.addProperty("enableNetworkMonitor", testConfig.enableNetworkMonitor);
jsonElement.addProperty("networkMonitorRule", testConfig.networkMonitorRule);
jsonElement.addProperty("enableTestOrchestrator", testConfig.enableTestOrchestrator);
jsonElement.addProperty("notifyUrl", testConfig.notifyUrl);

try {
Expand Down
2 changes: 2 additions & 0 deletions gradle_plugin/template/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ testScope =
testSuiteName =
# Required for test type: INSTRUMENTATION
testPkgName = # Absolute package name of the test app.
# Optional for test type: INSTRUMENTATION
enableTestOrchestrator = # Flag for whether to enable Android Test Orchestrator. Value: {false (Default), true}


# Optional for test type: SMART, MONKEY, APPIUM_MONKEY
Expand Down
3 changes: 2 additions & 1 deletion gradle_plugin/template/testSpec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,5 @@ testSpec:
disableRecording: # <Optional>
enableFailingTask: # <Optional>
enableNetworkMonitor: # <Optional>
networkMonitorRule: # <Optional>
networkMonitorRule: # <Optional>
enableTestOrchestrator: # <Optional>
7 changes: 7 additions & 0 deletions react/src/component/BaseView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ export default class BaseView extends React.Component {
});
console.log([event.target.name] + " => " + event.target.value)
}

handleValueSwitched = (event) => {
this.setState({
[event.target.name]: event.target.checked
});
console.log([event.target.name] + " => " + event.target.checked)
}

handleFileUpload = (event) => {
this.setState({
Expand Down
Loading

0 comments on commit d931f84

Please sign in to comment.