Appium [交流互助] 关于深度优先的探索性遍历工具的设计和实现

扫地僧 · 2016年05月07日 · 最后由 Jayvee 回复于 2017年05月23日 · 2438 次阅读

背景

最近在做 APP 性能自动化的需求,结合 Monkey 跑不靠谱,很想用思寒的工具,但是没有源码(反编译的效果不理想),对于控制欲强迫症的我只能在有限的时间重复造轮。
今天发这个帖子,主要是不想闭门造车,这里面肯定有我没考虑到的坑,希望各位这方面有经验的能给些意见和思路,避免少走弯路。

设计初稿

1.获取界面的布局(uiautomator dump 或者 appium getPageSource),解析后载入 clickable 为 true 的节点

2.定义 UiNode
a.id(node 唯一标识):MD5-class-locator
b.windowID(所属窗口) MD5
c.depth(窗口层级)
d.leftNode(父节点,只有 1 个)
e.rightNode(子节点,集合)

3.定义任务栈:Stack,采用先进后出原则

4.定义配置类:黑名单、遍历深度、遍历时间、引导规则(欢迎界面滑动、优先登录)

5.界面控件按默认规则操作,也支持自定义控件,以 android 为例:
标准控件:
android.widget.LinearLayout => click
android.widget.LinearLayout => click
android.widget.FrameLayout => click
android.widget.ImageView => click
android.widget.TextView => click
android.widget.Button => click
android.widget.EditText => input
android.webkit.WebView => context
自定义控件:
xxx.xxx.xxx => click
xxx.xxx.xxx => input

6.遍历步骤
a.如果设置了引导规则,优先完成欢迎界面和登录处理;
b.判断当前界面 native 还是 webview;
c.获取当前界面内容并解析;
d.识别界面是否发生迁移;当界面发生迁移时,将迁移后的界面替换为当前界面,并记录当前操作;当界面未发生迁移时,执行下一个步骤;
e.判断是否达到临界条件;当达到临界条件时,遍历结束,当未达到临界条件时,判断是否所有的界面都遍历完成;当所有的界面都遍历完成时,遍历结束;

7.动态分配端口,自动启动 Appium Server、生成思维导图、回放动画、测试报告等

讨论环节

1.根据当前节点获取 XPath
a.目前生成的是绝对路径,有没有生成相对路径的靠谱方法(看了几个都有问题)?
b.Android 的 node name 和 iOS 不同,有时候是控件类名,有时候是清一色的 node,是否是这样的?目前只能取 attribute 的 class value 生成

@Override
    public String getXPath(Node n) {
        String clazz1;
        String clazz2;
        NamedNodeMap nodeMap;

        if (null == n)
            return null;

        Node parent = null;
        Stack<Node> hierarchy = new Stack<Node>();
        StringBuffer buffer = new StringBuffer();

        hierarchy.push(n);

        switch (n.getNodeType()) {
            case Node.ATTRIBUTE_NODE:
                parent = ((Attr) n).getOwnerElement();
                break;
            case Node.ELEMENT_NODE:
                parent = n.getParentNode();
                break;
            case Node.DOCUMENT_NODE:
                parent = n.getParentNode();
                break;
            default:
                throw new IllegalStateException("Unexpected Node type" + n.getNodeType());
        }

        while (null != parent && parent.getNodeType() != Node.DOCUMENT_NODE) {
            hierarchy.push(parent);
            parent = parent.getParentNode();
        }

        Object obj = null;
        while (!hierarchy.isEmpty() && null != (obj = hierarchy.pop())) {
            Node node = (Node) obj;
            boolean handled = false;

            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element e = (Element) node;
                nodeMap = node.getAttributes();
                clazz1 = getAttribute(nodeMap, "class");

                if (buffer.length() == 0) {
                    buffer.append(clazz1);
                } else {
                    buffer.append("/");
                    buffer.append(clazz1);

                    if (!handled) {
                        int prev_siblings = 1;
                        Node prev_sibling = node.getPreviousSibling();
                        while (null != prev_sibling) {
                            if (prev_sibling.getNodeType() == node.getNodeType()) {
                                nodeMap = prev_sibling.getAttributes();
                                clazz2 = getAttribute(nodeMap, "class");
                                if (clazz2.equalsIgnoreCase(clazz1)) {
                                    prev_siblings++;
                                }
                            }
                            prev_sibling = prev_sibling.getPreviousSibling();
                        }
                        buffer.append("[" + prev_siblings + "]");
                    }
                }
            } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
                nodeMap = node.getAttributes();
                clazz1 = getAttribute(nodeMap, "class");
                buffer.append("/@");
                buffer.append(clazz1);
            }
        }
        return buffer.toString().replaceAll("null", "");
    }

2.基于深度优先的探索性遍历的算法(测试用骨架)
这里只是测试用的骨架,引导规则、黑名单、遍历时间、返回逻辑等未加入。
a.大致看了下,觉得返回逻辑可能存在一些坑,有没有好的设计思路供参考?
b.如果不做遍历深度的限制,Stack是否会导致内存溢出的风险?

public class AndroidEngine implements Engine {
    static Stack<UiNode> nodeStack = new Stack<UiNode>();

    public static void main(String[] args) {
        UiNode uiNode1 = new UiNode();
        uiNode1.setId("id1");
        uiNode1.setWindowID("win1");

        AndroidEngine test = new AndroidEngine();
        test.dfsSearch(uiNode1, 3);
    }

    /**
     * 模拟生成子节点
     *
     * @param depth
     * @param left
     * @return
     */
    public List<UiNode> getChild(int depth, UiNode left) {
        UiNode uiNode;
        List<UiNode> uiNodeList = new ArrayList<>();

        for (int i = 2; i > 0; i--) {
            uiNode = new UiNode();
            uiNode.setId("id" + depth + i);
            uiNode.setWindowID("win" + depth);
            uiNode.setDepth(depth);
            uiNode.setLeftNode(left);
            uiNodeList.add(uiNode);
        }
        return uiNodeList;
    }

    @Override
    public void dfsSearch(UiNode uiNode, int depth) {
        UiNode refNode = null;
        UiNode thisNode;
        List<UiNode> children = null;

        uiNode.setDepth(1);
        nodeStack.push(uiNode);

        while (!nodeStack.isEmpty()) {
            // 出栈
            thisNode = nodeStack.pop();

            if (thisNode.getLeftNode() != null) {
                System.out.println("left node: " + thisNode.getLeftNode().getWindowID() + " -> " + thisNode.getLeftNode().getId() + ", this node: " + thisNode.getWindowID() + " -> " + thisNode.getId());
            } else {
                System.out.println("left node: null, this node: " + thisNode.getWindowID() + " -> " + thisNode.getId());
            }

            // 遍历深度控制,0 表示未限制
            if (depth == 0 || thisNode.getDepth() < depth) {
                thisNode.setRightNode(getChild(thisNode.getDepth() + 1, thisNode));
                children = thisNode.getRightNode();

                // 入栈
                if (children != null && !children.isEmpty()) {
                    for (UiNode child : children) {
                        nodeStack.push(child);
                    }
                }
            }

            // 当前节点作为下一遍历节点的参照物
            refNode = thisNode;
        }
    }
}

public class UiNode {
    private boolean hasVisited = false;

    private int depth;

    private String id;

    private String windowID;

    private UiNode leftNode = null;

    private List<UiNode> rightNode = null;

    public int getDepth() {
        return depth;
    }

    public void setDepth(int depth) {
        this.depth = depth;
    }

    public String getWindowID() {
        return windowID;
    }

    public void setWindowID(String windowID) {
        this.windowID = windowID;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public UiNode getLeftNode() {
        return leftNode;
    }

    public void setLeftNode(UiNode leftNode) {
        this.leftNode = leftNode;
    }

    public List<UiNode> getRightNode() {
        return rightNode;
    }

    public void setRightNode(List<UiNode> rightNode) {
        this.rightNode = rightNode;
    }

    public boolean isHasVisited() {
        return hasVisited;
    }

    public void setHasVisited(boolean hasVisited) {
        this.hasVisited = hasVisited;
    }
}

3.如何生成思维导图
@seveniruby
如果有第三方插件支持,请提供传送门;
如果没有,那么是如何实现的,请给个解题思路;

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 10 条回复 时间 点赞

思维导图有自己的格式, 是 xml 的. 直接生成就行了

#1 楼 @seveniruby 调用第三方的 jar 包方法,还是 jdk 自带的方法,画图方面的之前没接触过,能否说的再详细点?

#2 楼 @quqing 没用第三方库 自己拼 freemind 的格式. 格式是固定的 xml 结构.

#3 楼 @seveniruby 之前以为是生成的图片,我想多了,谢谢

#3 楼 @seveniruby 再请教个问题,获取 Android 的界面的 xml,节点的 node name 有时候是控件类名,和 iOS 一致

有时候 node name 都是 node

为了求稳定,只能写两套方法,你有没有遇到这种情况?

#5 楼 @quqing xpath 完全可以用一个啊.//*[@class='android.widget.FrameLayout'] 就可以了

#6 楼 @seveniruby 我设计的可能不太一样,每个被操作对象的唯一标识用窗口 MD5-节点 class-xpath 组合标识,所以每个 node 的 xpath 在当前窗口是唯一的,奇怪的是 android 界面 dump 下来有上述情况,另,有好的设计建议也可供我参考下

扫地僧 [该话题已被删除] 中提及了此贴 07月04日 10:44

看了楼主的设计,思路可以,看具体实现需要花点时间了。还有隐藏不可见的怎么处理的?

扫地僧 自动遍历工具 Java 版 (开源) 中提及了此贴 11月24日 18:36

很好的设计思路分享,膜拜 ing

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册