-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Parser xml file to tree node and generate test case (#599)
<!-- 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 --> ## 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. --> - [ ] Tests for the changes have been added (for bug fixes / features) - [ ] 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 - [ ] 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 - [ ] 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...* --------- Co-authored-by: Le Zhou <[email protected]> Co-authored-by: dexterdreeeam <[email protected]>
- Loading branch information
1 parent
5727caf
commit 173a509
Showing
14 changed files
with
816 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
center/src/main/java/com/microsoft/hydralab/center/service/LongChainExample.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
112 changes: 112 additions & 0 deletions
112
...rc/main/java/com/microsoft/hydralab/center/service/generation/AbstractCaseGeneration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
107 changes: 107 additions & 0 deletions
107
...n/java/com/microsoft/hydralab/center/service/generation/MaestroCaseGenerationService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
.../main/java/com/microsoft/hydralab/center/service/generation/T2CCaseGenerationService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// 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.springframework.stereotype.Service; | ||
|
||
import java.io.File; | ||
import java.util.List; | ||
|
||
/** | ||
* @author zhoule | ||
* @date 07/21/2023 | ||
*/ | ||
|
||
@Service | ||
public class T2CCaseGenerationService extends AbstractCaseGeneration { | ||
@Override | ||
public File generateCaseFile(PageNode pageNode, List<PageNode.ExplorePath> explorePaths) { | ||
return null; | ||
} | ||
|
||
@Override | ||
public File generateCaseFile(PageNode pageNode, PageNode.ExplorePath explorePaths, File caseFolder) { | ||
return null; | ||
} | ||
} |
Empty file.
Empty file.
Empty file.
Oops, something went wrong.