Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft: parser xml file to tree node and generate test case #535

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
9 changes: 6 additions & 3 deletions center/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ tasks.withType(JavaCompile) {
}

dependencies {
testImplementation 'org.mockito:mockito-core:3.12.4'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootWebVersion
testImplementation 'me.paulschwarz:spring-dotenv:2.3.0'
compileOnly 'dev.langchain4j:langchain4j:0.11.0'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why using compileOnly?

compileOnly 'dev.langchain4j:langchain4j-pinecone:0.11.0'
implementation 'org.dom4j:dom4j:2.1.4'
testCompile 'org.mockito:mockito-core:3.12.4'
testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootWebVersion
testCompile 'me.paulschwarz:spring-dotenv:2.3.0'

compile project(":common")
compile project(":taps_to_cases:runner")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.alibaba.fastjson.JSONObject;
import com.microsoft.hydralab.center.service.StorageTokenManageService;
import com.microsoft.hydralab.center.service.TestDataService;
import com.microsoft.hydralab.center.service.generation.MaestroCaseGenerationService;
import com.microsoft.hydralab.common.entity.agent.Result;
import com.microsoft.hydralab.common.entity.center.SysUser;
import com.microsoft.hydralab.common.entity.common.AndroidTestUnit;
Expand All @@ -26,9 +27,11 @@
import com.microsoft.hydralab.common.util.FileUtil;
import com.microsoft.hydralab.common.util.HydraLabRuntimeException;
import com.microsoft.hydralab.common.util.LogUtils;
import com.microsoft.hydralab.common.util.PageNode;
import com.microsoft.hydralab.t2c.runner.T2CJsonGenerator;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Assertions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
Expand All @@ -47,11 +50,13 @@
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import static com.microsoft.hydralab.center.util.CenterConstant.CENTER_TEMP_FILE_DIR;
Expand All @@ -72,6 +77,8 @@ public class TestDetailController {
AttachmentService attachmentService;
@Resource
StorageServiceClientProxy storageServiceClientProxy;
@Resource
MaestroCaseGenerationService maestroCaseGenerationService;

/**
* Authenticated USER:
Expand Down Expand Up @@ -412,4 +419,52 @@ public Result<String> generateT2CJsonFromSmartTest(@CurrentSecurityContext SysUs
return Result.ok(t2cJson);
}

@GetMapping(value = {"/api/test/generateMaestro/{fileId}"}, produces = MediaType.APPLICATION_JSON_VALUE)
public Result generateMaestroFromSmartTest(@CurrentSecurityContext SysUser requestor,
@PathVariable(value = "fileId") String fileId,
@RequestParam(value = "testRunId") String testRunId,
HttpServletResponse response) throws IOException {
if (requestor == null) {
return Result.error(HttpStatus.UNAUTHORIZED.value(), "unauthorized");
}

File graphZipFile = loadGraphFile(fileId);
File graphFile = new File(graphZipFile.getParentFile().getAbsolutePath(), Const.SmartTestConfig.GRAPH_FILE_NAME);
TestRun testRun = testDataService.findTestRunById(testRunId);
TestTask testTask = testDataService.getTestTaskDetail(testRun.getTestTaskId());

PageNode rootNode = maestroCaseGenerationService.parserXMLToPageNode(graphFile.getAbsolutePath());
Assertions.assertNotNull(rootNode, "parser xml to page node failed");
rootNode.setPageName(testTask.getPkgName());
System.out.println(rootNode);
List<PageNode.ExplorePath> explorePaths = new ArrayList<>();
maestroCaseGenerationService.explorePageNodePath(rootNode, "", "", explorePaths);
File caseZipFile = maestroCaseGenerationService.generateCaseFile(rootNode, explorePaths);

if (caseZipFile == null) {
return Result.error(HttpStatus.BAD_REQUEST.value(), "The file was not downloaded");
}
try {
FileInputStream in = new FileInputStream(caseZipFile);
ServletOutputStream out = response.getOutputStream();
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + caseZipFile.getName());
int len;
byte[] buffer = new byte[1024 * 10];
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.flush();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
response.flushBuffer();
caseZipFile.delete();
}

return Result.ok();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package com.microsoft.hydralab.center.service;

import dev.langchain4j.model.input.structured.StructuredPrompt;

/**
* @author zhoule
* @date 07/13/2023
*/

public class LongChainExample {

@StructuredPrompt({
"I want you to act as a software tester. I will provide a route map of a mobile application and it will be your job to write a test case. ",
"The case should be in maestro script format. This is a maestro example",
"{{maestroExample}}",
"Firstly I will introduce the format of the route map.",
"1. It is a unidirectional ordered graph in xml format, the nodes attribute are the pages of app and the id property of each node is the unique id of page. " +
"By the way the id of node equals -1 means the app has not been opened.",
"2. The edges attributes means the only way of jumping from a page to another page. The source property is the unique id of original page and the target property " +
"is the unique id of the page after jumping. The attvalue of each edge means the operation type such launch app, click button, click testview etc..",
"The commands that maestro supported is in the site https://maestro.mobile.dev/api-reference/commands.",
"Requirements:",
"1. the case should start from node which id is -1.",
"2. the case must follow the direction of the edge.",
"3. the case should jump as many pages as possible of the app.",
"4. the page can be visited only once",
"5. you can't use the back command",
"6. add comment to case declare current page id",
"The first route map is {{routeMap}}",
"please generate a maestro script for this route map."
})
static class MaestroCaseGeneration {

String maestroExample;
String routeMap;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package com.microsoft.hydralab.center.service.generation;

import com.microsoft.hydralab.common.util.PageNode;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.util.StringUtils;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* @author zhoule
* @date 07/21/2023
*/

public abstract class AbstractCaseGeneration {
public PageNode parserXMLToPageNode(String xmlFilePath) {
// read xml file, get page node and action info
Document document = null;
SAXReader saxReader = new SAXReader();
try {
document = saxReader.read(xmlFilePath);
} catch (DocumentException e) {
throw new RuntimeException(e);
}
List<Element> pages = document.getRootElement().element("graph").element("nodes").elements("node");
List<Element> actions = document.getRootElement().element("graph").element("edges").elements("edge");

Map<Integer, PageNode> pageNodes = new HashMap<>();
// init page node
for (Element page : pages) {
PageNode pageNode = new PageNode();
int id = Integer.parseInt(page.attributeValue("id"));
pageNode.setId(id);
pageNodes.put(id, pageNode);
}
// init action info
for (Element action : actions) {
int source = Integer.parseInt(action.attributeValue("source"));
int target = Integer.parseInt(action.attributeValue("target"));
if (source == target) {
continue;
}
int actionId = Integer.parseInt(action.attributeValue("id"));
//link action to page
pageNodes.get(source).getActionInfoList().add(parserAction(action));
//link page to page
pageNodes.get(source).getChildPageNodeMap().put(actionId, pageNodes.get(target));
}
return pageNodes.get(0);
}

private PageNode.ActionInfo parserAction(Element element) {
PageNode.ActionInfo actionInfo = new PageNode.ActionInfo();
Map<String, Object> arguments = new HashMap<>();
actionInfo.setId(Integer.parseInt(element.attributeValue("id")));
actionInfo.setActionType("click");

PageNode.ElementInfo elementInfo = new PageNode.ElementInfo();
String sourceCode = element.element("attvalues").element("attvalue").attributeValue("value");
elementInfo.setText(extractElementAttr("Text", sourceCode));
elementInfo.setClassName(extractElementAttr("Class", sourceCode));
elementInfo.setClickable(Boolean.parseBoolean(extractElementAttr("Clickable", sourceCode)));
elementInfo.setResourceId(extractElementAttr("ResourceID", sourceCode));
actionInfo.setTestElement(elementInfo);
if (!StringUtils.isEmpty(elementInfo.getText())) {
arguments.put("defaultValue", elementInfo.getText());
} else if (!StringUtils.isEmpty(elementInfo.getResourceId())) {
arguments.put("id", elementInfo.getResourceId());
}
actionInfo.setArguments(arguments);
return actionInfo;
}

private String extractElementAttr(String attrName, String elementStr) {
String[] attrs = elementStr.split(attrName + ": ");
if (attrs.length > 1 && !attrs[1].startsWith(",")) {
return attrs[1].split(",")[0];
}
return "";
}

/**
* explore all path of page node
*
* @param pageNode
* @param nodePath
* @param action
* @param explorePaths
*/
public void explorePageNodePath(PageNode pageNode, String nodePath, String action, List<PageNode.ExplorePath> explorePaths) {
if (pageNode.getChildPageNodeMap().isEmpty()) {
explorePaths.add(new PageNode.ExplorePath(nodePath + "_" + pageNode.getId(), action));
return;
}
for (Map.Entry<Integer, PageNode> entry : pageNode.getChildPageNodeMap().entrySet()) {
explorePageNodePath(entry.getValue(), StringUtils.isEmpty(nodePath) ? String.valueOf(pageNode.getId()) : nodePath + "_" + pageNode.getId(),
StringUtils.isEmpty(action) ? String.valueOf(entry.getKey()) : action + "," + entry.getKey(), explorePaths);
}
}

public abstract File generateCaseFile(PageNode pageNode, List<PageNode.ExplorePath> explorePaths);

public abstract File generateCaseFile(PageNode pageNode, PageNode.ExplorePath explorePaths, File caseFolder);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package com.microsoft.hydralab.center.service.generation;

import com.microsoft.hydralab.center.util.CenterConstant;
import com.microsoft.hydralab.common.util.DateUtil;
import com.microsoft.hydralab.common.util.FileUtil;
import com.microsoft.hydralab.common.util.HydraLabRuntimeException;
import com.microsoft.hydralab.common.util.PageNode;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
* @author zhoule
* @date 07/14/2023
*/

@Service
public class MaestroCaseGenerationService extends AbstractCaseGeneration {
/**
* generate maestro case files and zip them
*
* @param pageNode
* @param explorePaths
* @return
*/
@Override
public File generateCaseFile(PageNode pageNode, List<PageNode.ExplorePath> explorePaths) {
// create temp folder to store case files
File tempFolder = new File(CenterConstant.CENTER_TEMP_FILE_DIR, DateUtil.fileNameDateFormat.format(new Date()));
if (!tempFolder.exists()) {
tempFolder.mkdirs();
}
// generate case files
for (PageNode.ExplorePath explorePath : explorePaths) {
generateCaseFile(pageNode, explorePath, tempFolder);
}
if (tempFolder.listFiles().length == 0) {
return null;
}
// zip temp folder
File zipFile = new File(tempFolder.getParent() + "/" + tempFolder.getName() + ".zip");
FileUtil.zipFile(tempFolder.getAbsolutePath(), zipFile.getAbsolutePath());
FileUtil.deleteFile(tempFolder);
return zipFile;
}

@Override
public File generateCaseFile(PageNode pageNode, PageNode.ExplorePath explorePath, File caseFolder) {
File maestroCaseFile = new File(caseFolder, explorePath.getPath() + ".yaml");
String caseContent = buildConfigSection(pageNode.getPageName());
caseContent += buildDelimiter();
caseContent += buildCommandSection("launch", null);
String[] actionIds = explorePath.getActions().split(",");
PageNode pageNodeCopy = pageNode;
for (String actionId : actionIds) {
PageNode.ActionInfo action = pageNodeCopy.getActionInfoList().stream().filter(actionInfo -> actionInfo.getId() == Integer.parseInt(actionId)).findFirst().get();
caseContent += buildCommandSection(action.getActionType(), action.getArguments());
pageNodeCopy = pageNodeCopy.getChildPageNodeMap().get(Integer.parseInt(actionId));
}
caseContent += buildCommandSection("stop", null);
FileUtil.writeToFile(caseContent, maestroCaseFile.getAbsolutePath());
return maestroCaseFile;
}

private String buildConfigSection(String appId) {
return "appId: " + appId + "\n";
}

private String buildDelimiter() {
return "---\n";
}

private String buildCommandSection(String actionType, Map<String, Object> arguments) {
String command = "-";
switch (actionType) {
case "launch":
command = command + " launchApp\n";
break;
case "click":
command = command + " tapOn:";
if (arguments.size() == 0) {
throw new HydraLabRuntimeException("arguments is empty");
}
if (arguments.containsKey("defaultValue")) {
command = command + " " + arguments.get("defaultValue") + "\n";
break;
}
command = command + "\n";
for (String key : arguments.keySet()) {
command = command + " " + key + ": \"" + arguments.get(key) + "\"\n";
}
break;
case "stop":
command = command + " stopApp\n";
break;
default:
throw new HydraLabRuntimeException("Unsupported action type: " + actionType);
}
return command;
}
}
Loading
Loading