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

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

关于增量代码覆盖率

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

image

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

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

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

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

eclipse JDT

我们可以来看个例子
image

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

image

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

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

image

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

image

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

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

kastree

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

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

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

image

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 覆盖率的时候还遇到了很多的问题,比如

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

参考文章

增量代码覆盖率工具

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


↙↙↙阅读原文可查看相关链接,并与作者交流