1. STest软件测试社区首页
  2. 测试工具

解决 jacoco 支持增量 kotlin 代码覆盖率

从年前到现在终于将代码覆盖率从0到1,做成了平台化,并且将它落地到大部分的项目测试中。这个是个人过去一年来最大的收获了

从年前到现在终于将代码覆盖率从0到1,做成了平台化,并且将它落地到大部分的项目测试中。这个是个人过去一年来最大的收获了。

首先我们在讲这个标题的时候,我们先要明确一点jacoco本身的代码覆盖率是支持kotlin的。主要不支持的是因为我们很多二次开发jacoco后,支持了增量的代码覆盖率以后才出现有这样子的问题。

关于增量代码覆盖率

那既然聊到增量代码覆盖率,我们就先说说现在大部分的增量代码覆盖率是怎么实现的吧。

解决 jacoco 支持增量 kotlin 代码覆盖率

我们先看下上面这张图,这个是引用了有赞的 增量代码覆盖率工具 文中提到的原理图。

文中已经详细说到了大体的流程:

针对获取基线提交与被测提交之间的差异代码,进行解析将变更代码解析到方法纬度

这里的关键就是拿到差异的文件内容以后,怎么将普通的一个字符串文本转换成AST(abstract syntax code,AST) 抽象语法树的过程,上述的文章没有提及到,不过有幸找到另外一个github项目 JacocoPlus,其实目前平台的jacoco核心的代码都是基于它来做二次开发的,大家有兴趣可以了解下这个项目。在 ASTGenerator 类中就实现了将java 源码转换成AST, 而这其中真正起作用的是eclipse jdt。

eclipse JDT

我们可以来看个例子
解决 jacoco 支持增量 kotlin 代码覆盖率

以上是我们的一个java代码的范例。那通过eclipse jdt 解析后会是什么样呢?

解决 jacoco 支持增量 kotlin 代码覆盖率

通过上图就能够看出来,java的代码中每个方法都被解析出来了,通过还包括了参数以及它的返回值等等。

那我们再看看当它遇到kotlin的代码后,又是怎么样的表现呢?

解决 jacoco 支持增量 kotlin 代码覆盖率

以上是我们的一段kotlin的代码。

解决 jacoco 支持增量 kotlin 代码覆盖率

解析以后,我们看红色框处的部分。确实解析出来了相应的方法名称,可是我们很明显能够发现,它直接将fun 当成了返回值了。然后真正的返回是后面的any。

所以很明显JDT 是已经不能够胜任kotlin的解析的工作了。

kastree

在github上搜索了一番,找到了一丝的希望 kastree。虽然项目已经说明了不再维护了,不过至少可以抱着试试的态度嘛。

关于如何使用我就不在这里说明了,大家可以自己去到项目地址上去了解,或者文末部分的代码

重新拿了前面的代码我们再试了一遍

解决 jacoco 支持增量 kotlin 代码覆盖率

file变量 实际上就是解析出来的结果,从上图可以看出来,其实他已经解析到了方法名称,以及参数类型及内容。只是他的解析的结构跟jdt的结构差距很大。所以这里我们自己可能需要做一些处理。

这里附上根据ASTGenerator 实现的 KotlinASTGenerator的代码。

/*******************************************************************************
 * Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Marc R. Hoffmann - initial API and implementation
 *
 *******************************************************************************/
package org.jacoco.core.internal.diff;

import kastree.ast.Node;
import kastree.ast.psi.Converter;
import kastree.ast.psi.Parser;
import sun.misc.BASE64Encoder;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class KotlinASTGenerator {
    private Node.File file;
    private String filePath;
    public static final Parser parser = new Parser();
    public KotlinASTGenerator(String kotlinText, String filePath) {
        this.filePath = filePath;
        file = parser.parseFile(kotlinText, false);

    }


    /**
 * 获取kotlin类包名
 * @return
 */
    public String getPackageName() {
        if (file == null) {
            return "";
        }
        StringBuilder convertedListStr = new StringBuilder();
        int index = 0;
        for (String pkg: file.getPkg().getNames()) {
            index ++;
            if (index < file.getPkg().getNames().size()) {
                convertedListStr.append(pkg).append(".");
            }else {
                convertedListStr.append(pkg);
            }

        }
        return convertedListStr.toString();
    }

    /**
 * 获取普通类单元
 * @return
 */
    public String getJavaClass() {
        if (file == null) {
            return null;
        }
        if (file.getDecls().size() > 0) {
            if (file.getDecls().get(0).getClass().toString().equals("class kastree.ast.Node$Decl$Structured")) {
                return ((Node.Decl.Structured)file.getDecls().get(0)).getName();
            }else {
                // 这里可能全部都是方法,没有定义类的概念,所以要处理下
                return (filePath.substring(filePath.lastIndexOf("/") + 1, filePath.lastIndexOf(".")));
            }

        }else {
            return null;
        }
    }

    /**
 * 获取kotlin类中所有方法
 * @return 类中所有方法
 */
    public List<Node.Decl.Func> getMethods() {
        List<Node.Decl.Func> funcs = new ArrayList<Node.Decl.Func>();
        for( Node.Decl decl: file.getDecls()) {
            if (decl.getClass().toString().equals("class kastree.ast.Node$Decl$Structured")) {
                for (Node.Decl decl1 : ((Node.Decl.Structured)decl).getMembers()) {
                    if (decl1.getClass().toString().equals("class kastree.ast.Node$Decl$Func")) {
                        funcs.add((Node.Decl.Func) decl1);
                    }else if (decl1.getClass().toString().equals("class kastree.ast.Node$Decl$Structured")) {
                        for (Node.Decl decl2 : ((Node.Decl.Structured)decl1).getMembers()) {
                            if (decl2.getClass().toString().equals("class kastree.ast.Node$Decl$Func")) {
                                funcs.add((Node.Decl.Func) decl2);
                            }
                        }
                    }
                }
            }else if (decl.getClass().toString().equals("class kastree.ast.Node$Decl$Func")) {
                funcs.add((Node.Decl.Func)decl);
            }else {
                System.out.println(decl.getClass().toString());
            }
        }
        return funcs;
    }


    /**
 * 获取修改类型的类的信息以及其中的所有方法,排除接口类
 * @param methodInfos
 * @param addLines
 * @param delLines
 * @return
 */
    public ClassInfo getClassInfo(List<MethodInfo> methodInfos, List<int[]> addLines, List<int[]> delLines, String filePath) {
        if (getJavaClass() == null) {
            return null;
        }
        ClassInfo classInfo = new ClassInfo();
        classInfo.setClassName(getJavaClass());
        classInfo.setPackages(getPackageName());
        classInfo.setMethodInfos(methodInfos);
        classInfo.setAddLines(addLines);
        classInfo.setDelLines(delLines);
        classInfo.setType("REPLACE");
        classInfo.setNewFilePath(filePath);
        return classInfo;
    }

    /**
 * 获取新增类型的类的信息以及其中的所有方法,排除接口类
 * @return
 */
    public ClassInfo getClassInfo(String filePath, List<int[]> addLines, List<int[]> delLines) {
        if (getJavaClass() == null) {
            return null;
        }
        List<Node.Decl.Func> methodDeclarations = getMethods();
        ClassInfo classInfo = new ClassInfo();
        classInfo.setClassName(getJavaClass());
        classInfo.setPackages(getPackageName());
        classInfo.setType("ADD");
        classInfo.setAddLines(addLines);
        classInfo.setDelLines(delLines);
        classInfo.setNewFilePath(filePath);
        List<MethodInfo> methodInfoList = new ArrayList<MethodInfo>();
        for (Node.Decl.Func method: methodDeclarations) {
            MethodInfo methodInfo = new MethodInfo();
            setMethodInfo(methodInfo, method);
            methodInfoList.add(methodInfo);
        }
        classInfo.setMethodInfos(methodInfoList);
        return classInfo;
    }

    /**
 * 获取修改中的方法
 * @param methodDeclaration
 * @return
 */
    public MethodInfo getMethodInfo(Node.Decl.Func methodDeclaration) {
        MethodInfo methodInfo = new MethodInfo();
        setMethodInfo(methodInfo, methodDeclaration);
        return methodInfo;
    }

    private void setMethodInfo(MethodInfo methodInfo, Node.Decl.Func methodDeclaration) {
        methodInfo.setMd5(methodDeclaration.getBody() == null ? "" : MD5Encode(methodDeclaration.getBody().toString()));
        methodInfo.setMethodName(methodDeclaration.getName());
        methodInfo.setParameters(methodDeclaration.getParams().toString());
    }


    /**
 * 计算方法的MD5的值
 * @param s
 * @return
 */
    public static String MD5Encode(String s) {
        String MD5String = "";
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            BASE64Encoder base64en = new BASE64Encoder();
            MD5String = base64en.encode(md5.digest(s.getBytes("utf-8")));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return MD5String;
    }

    /**
 * 判断方法是否存在
 * @param method 新分支的方法
 * @param methodsMap master分支的方法
 * @return
 */
    public static boolean isMethodExist(final Node.Decl.Func method, final Map<String, Node.Decl.Func> methodsMap) {
        // 方法名+参数一致才一致
        if (!methodsMap.containsKey(method.getName() + method.getParams().toString() + (method.getReceiverType() == null ? "" : method.getReceiverType().toString()))) {
            return false;
        }
        return true;
    }

    /**
 * 判断方法是否一致
 * @param method1
 * @param method2
 * @return
 */
    public static boolean isMethodTheSame(final Node.Decl.Func method1, final Node.Decl.Func method2) {
        if (method1.getBody() == null || method2.getBody() == null) {
            return true;
        }else if (method1.getBody().toString().equals(method2.getBody().toString())) {
            return true;
        }
        return false;
    }
}

通过如上的代码,我们就可以做到,区分java以及kotlin的代码走不同的解析逻辑来完成这个事情了。

结束语

其实上面只是讲了关于kotlin这块增量代码覆盖率的解决,其实在做jacoco覆盖率的时候还遇到了很多的问题,比如

  • 多模块的代码覆盖率问题
  • 如何同时生成增量以及全量的测试报告
  • jacoco 解析jar 遇到 Can’t add different class with same name 又是什么原因等等。

只能说只有真正用到项目了,使用起来了,问题也就接踵而来。

参考文章

增量代码覆盖率工具

Java 覆盖率 Jacoco 插桩的不同形式总结和踩坑记录

原创文章,作者:测试媛,如若转载,请注明出处:http://www.stest.com

发表评论

电子邮件地址不会被公开。 必填项已用*标注

QR code