精华内容
下载资源
问答
  • C++代码覆盖率计算

    2018-11-22 19:50:27
    Windows下一般用opencppcoverage计算C++代码覆盖率。 可以直接在Visual Studio上安装 也可以在cmd上运行OpenCppCoverage.exe来实现。 OpenCppCoverage.exe --optimized_build --plugin --sources D:\...

    Windows下一般用opencppcoverage计算C++代码覆盖率。
    可以直接在Visual Studio上安装
    VS上之间安装
    也可以在cmd上运行OpenCppCoverage.exe来实现。
    在这里插入图片描述

    OpenCppCoverage.exe --optimized_build --plugin 
    --sources D:\TALLivePlatform\TALLivePlatform\
    -- D:\TALLivePlatform\TALLivePlatform\build\x64\Release\TALLivePlatform.exe
    

    可以直接生成html,包含数值和饼状图,以及代码覆盖颜色标注。
    在这里插入图片描述
    Linux下可以使用命令
    gcov lcov 覆盖c/c++项目入门

    cd /data/workspace/C++Coverage/code/ooo;
    g++ -c /data/workspace/C++Coverage/code/ooo/oo.cpp -ftest-coverage -fprofile-arcs;
    g++ /data/workspace/C++Coverage/code/ooo/oo.o -o /data/workspace/C++Coverage/code/ooo/oo --coverage;
    /data/workspace/C++Coverage/code/ooo/./oo;
    gcov /data/workspace/C++Coverage/code/ooo/oo.cpp;
    lcov -c -o /data/workspace/C++Coverage/code/ooo/oo.info -d .;
    genhtml /data/workspace/C++Coverage/code/ooo/oo.info -o /data/workspace/C++Coverage/code/ooo/oo_result"
    

    Jenkins上结果展示
    可以看到语句被覆盖的次数在行头有标注。

    展开全文
  • 因为某项目的白盒单元测试做的不是很好,而QA又提出了非常高的代码覆盖率移行标准,不得以开始调研能覆盖手工测试(Mannual Test)的覆盖率工具,加上一些最基础的条件,该工具必须满足如下三点: 1:能计算白盒...

    因为某项目的白盒单元测试做的不是很好,而QA又提出了非常高的代码覆盖率移行标准,不得以开始调研能覆盖手工测试(Mannual Test)的覆盖率工具,加上一些最基础的条件,该工具必须满足如下三点:

    1:能计算白盒单元测试覆盖率;

    2:能计算黑盒手工测试覆盖率;

    3:支持MSTest框架;

    第一个自然想到的就是NCover,没错,它肯定满足这三个条件,它有一个满足21天使用的Trial版本可以用,但是,商业版本的价格也是惊人的:

    NCover Code Central$ 2,298
    NCover Desktop$ 658
    NCover Collector$ 448

    很多刀刀。相比而言,生产ResharperJetbrainsDotCover,价格就要便宜很多,Personal License只要99刀,即便商业授权也只要199刀。当然,我现在还发现一个更龌蹉的方法,就是使用Classroom版本,然后到期了改时间就OKDotCover现在是鼓励大家合理使用。

    闲话少说,那么到底它支持不支持如上3个标准(实际上,最重要的是:是否良好支持手工测试覆盖率计算)。下载Classroom liscence,安装之(目前已支持VS2012)。DotCover是基于VS插件的形式存在的,安装完毕,我们会在VS的工具栏中发现DotCover的标识,如下,比如,启动之“Cover Application”:

    就会出现如下界面:

    可以看到,它支持的应用种类还是比较多的。如果是启动普通的应用程序,如Winform,那么操作起来还是比较简单的,点击“Standalone Application”后,就会出现如下界面:

    按照提示,输入后,OK-runDotCover就会为我们启动应用程序,当然,随着启动应用程序,DotCover的主模块也被启动起来,如下:

    然后随着我们使用该应用程序(也就是测试),DotCover会跟踪代码执行的路径。测试完毕,我们“Get Snapshot”,最终DotCover会为我们生成如下的报告:

    覆盖率结果清清楚楚的列在最后一列。

    测试网站,要稍稍复杂一点,因为我们要将DotCover部署在WEB服务器上。由于它是以VS插件的形式存在的,首先WEB服务器上就得有VS(注意,Express版本可是不行滴)。启动DotCover还是一样的,但是最主要的,我们在选择Application Type的时候,要选择“IIS Application”,然后在弹出的界面里,URL可以空着,Run webbrowser也可以不勾选(因为我们又不是要在服务器上测试网站),最最重要的,是在“Edit Filter”的弹出界面中把网站的Bin路径配置进去。注意,如果我们的站点有多个虚拟路径的话,则需要把全部虚拟路径的Bin目录配置进去(以分号隔开)(下面的那些checkbox都可以不再勾选)。配置完毕,点“Run”。

    然后,我们回到某个客户端上,启动浏览器,输入网站地址,当第一个人启动网站后,我们就会发现,在服务器的这个界面:

    中的Process栏目,会变成w3wp这个进程,如果这个进程出现了,那么说明DotCover开始工作了。

    最后,有几点需要进行说明:

    1:每次测试完毕,应该将Snapshot保存为独立的文件,最终每次测试的结果我们可以使用DotCover的命令行工具进行合并,示例如下:

            DotCover merge /source=d:\1.dcvr;d:\2.dcvr /output=d:\end.dcvr /tempdir=d:\

    2:其次,当前的Release版本为2.1.xxx,恭喜你,安装在Win7上毫无问题,但是,当你安装在XP或者Window Server2003版本上的时候,会发现:

            “Can't initialize profiler. PDB server wasn't started."

    这个时候,我们就要去JetBrainsDailybuider版本的地址下载最新的安装包了,在这里:http://confluence.jetbrains.net/display/DCVR/dotCover+Early+Access+ProgramJetBrains号称2.2版本已经修复了该问题。是的,它确实已经修复了,但是还有些别的小问题,如果你不幸遇到了"failed to cocreate profiler",一定记得来问我是如何龌蹉滴解决了该问题的。

    当然,所有这一些,我相信2.2Release版本出来后一定会解决的,毕竟,牛公司就得有个牛公司的风范。

     

    总之,DotCover还是一款非常不错的覆盖率计算的工具软件,解决了开发、测试、QA三方面的问题(PS,白盒的覆盖率就不介绍了,毕竟那还是最基本的功能)。

     

     

    展开全文
  • 针对前端代码覆盖率并不能像java那块那么简单,有专门的javascript的解析器,能够获取到这个js文件中所有的方法。所以套用原有的java那套逻辑基本是不太可行的。所以我们需要另辟蹊径来解决这个问题。 java的增量...

    关于后台的代码增量的逻辑已经有比较成熟的方案了。 根据javaparser解析前后的文件的方法列表,判断是否有新增或者修改的方法。

    前端代码覆盖率增量覆盖的困难

    针对前端代码覆盖率并不能像java那块那么简单,有专门的javascript的解析器,能够获取到这个js文件中所有的方法。所以套用原有的java那套逻辑基本是不太可行的。所以我们需要另辟蹊径来解决这个问题。

    java的增量代码diff 我们是从解析源码的文件入手的,那针对js既然这套不行,有没有方式能够从覆盖率结果数据入手,去解决这个事情呢?
    如果了解前端代码覆盖率的同学可能都清楚,前端的覆盖率收集是根据浏览器提交的coverage数据来的。而coverage的数据其实是大有来头的。

    我们看一个数据

    
    /**
     *
     * * `path` - the file path for which coverage is being tracked
     * * `statementMap` - map of statement locations keyed by statement index
     * * `fnMap` - map of function metadata keyed by function index
     * * `branchMap` - map of branch metadata keyed by branch index
     * * `s` - hit counts for statements
     * * `f` - hit count for functions
     * * `b` - hit count for branches
     */
    {
        path: filePath,
        statementMap: {
            "5": {
    			"end": {
    				"line": 10,
    				"column": 30
    			},
    			"start": {
    				"line": 10,
    				"column": 26
    			}
    		},
        },
        fnMap: {},
        branchMap: {},
        s: {
            "5": 10290,
        },
        f: {},
        b: {}
    }
    

    以上的内容中statementMap 中的5代表标记的对应的代码块为第10行,列则是从26到30, 同时映射到s中 这个代码块被执行了10290次。
    这块的数据其实也完整的说明了对应的文件中,代码/分支/方法是否覆盖的情况。

    思路

    那么是不是可以这么去考虑呢?从git diff中对比得到对应文件的改动行数,然后再对应到这块的数据上,如果修改的代码行 是在statemanMap/ fnMap/ branchMap 的覆盖范围的话就保留这块的数据,如果说改动行中,不存在有这块的内容则从对象中将这块的内容剔除掉。这样子就可以得到增量的数据了。

    但是我们是不是直接针对用户提交的coverage数据做处理呢?

    答案是不行的。 我们需要了解一个问题,之前我们在 聊聊前端代码覆盖率 (长文慎入) 中提到过用户提交的coverage数据并不是完整的反应到原本的代码行上,主要是针对typescript这块,因为如果你的编译是经过ts-loader -> babel-loader 处理的话。得到的coverage中的数据中的line的值,其实跟源码中的line会出现不一致的情况。而istanbul这块是会根据sourceMap 重新映射回去的。

    那哪里的数据才是正在正确的呢? 答案其实在通过nyc api生成的报告目录下, 当你的api指定了reporter包含有 json的情况下,就会在覆盖率报告的目录下生成有 coverage-final.json。 这里的数据其实跟coverage数据基本是一致的,并且这里的数据已经经过istanbul校正过。所以我们可以信任这块的数据。

    解决

    从git api中获取到改动行,判断statemanMap/ fnMap/ branchMap 的开始行及结束行的范围是否包含了改动行。如果在对应的范围那么则保留对象数据,如果不在,则移除掉对应的对象。如此剩下的就是改动范围的覆盖情况

    所以我们可以这么处理

    /**
     * 根据代码codeDiff,过滤掉未改动的语句
     * @param fileFinal 对应文件的覆盖率数据情况,格式就是我们上述提到的数据
     * @param statements 
     * @return
     */
    private void handleStatements(List<Integer> codeDiff, Coverage fileFinal, CoverageSummary statements, CoverageSummary lines) {
        List<String> keys = new ArrayList<>();
        Map<Integer, Integer> line = new HashMap<>();
        // 这里的key 是0, 1, 2, 3  "statementMap":{"0":{"start":{"line":9,"column":22},"end":{"line":39,"column":1}} ;; "s":{"0":10150,"1":10150,"2":0,"3":0},
        for (String key : fileFinal.getStatementMap().keySet()) {
            if (!isDiff(codeDiff, fileFinal.getStatementMap().get(key).getStart().getLine(), fileFinal.getStatementMap().get(key).getEnd().getLine())) {
                keys.add(key);
            }
        }
    
        // 将不包含改动行的桩删除
        for (String key : keys) {
            fileFinal.getStatementMap().remove(key);
            fileFinal.getS().remove(key);
        }
    
        computeCoverageSummary(getLineCoverage(fileFinal), lines);
        computeCoverageSummary(fileFinal.getS(), statements);
    }
    

    这里关于statement/line/fun的统计计算这里就不详细描述了,主要可以参考 file-coverage 中的各个值的计算逻辑。

    所以我们举一个简单的例子来说明下:假设 client/src/utils/location.js 的coverage数据如下:

    {
        "/app/thirdCode/c94933a0-7250-11eb-8c93-c9620716ef34_1/xxx/client/src/utils/location.js": {
            "path": "/app/thirdCode/c94933a0-7250-11eb-8c93-c9620716ef34_1/xxx/client/src/utils/location.js",
            "statementMap": {
                "0": {
                    "start": {
                        "line": 9,
                        "column": 39
                    },
                    "end": {
                        "line": 9,
                        "column": 54
                    }
                },
                "1": {
                    "start": {
                        "line": 10,
                        "column": 25
                    },
                    "end": {
                        "line": 10,
                        "column": 52
                    }
                },
                "2": {
                    "start": {
                        "line": 11,
                        "column": 26
                    },
                    "end": {
                        "line": 11,
                        "column": 48
                    }
                },
                "3": {
                    "start": {
                        "line": 13,
                        "column": 4
                    },
                    "end": {
                        "line": 19,
                        "column": 5
                    }
                },
                "4": {
                    "start": {
                        "line": 14,
                        "column": 8
                    },
                    "end": {
                        "line": 17,
                        "column": 9
                    }
                },
                "5": {
                    "start": {
                        "line": 15,
                        "column": 12
                    },
                    "end": {
                        "line": 15,
                        "column": 37
                    }
                },
                "6": {
                    "start": {
                        "line": 16,
                        "column": 12
                    },
                    "end": {
                        "line": 16,
                        "column": 21
                    }
                },
                "7": {
                    "start": {
                        "line": 18,
                        "column": 8
                    },
                    "end": {
                        "line": 18,
                        "column": 37
                    }
                },
                "8": {
                    "start": {
                        "line": 20,
                        "column": 22
                    },
                    "end": {
                        "line": 20,
                        "column": 45
                    }
                },
                "9": {
                    "start": {
                        "line": 21,
                        "column": 20
                    },
                    "end": {
                        "line": 21,
                        "column": 72
                    }
                },
                "10": {
                    "start": {
                        "line": 23,
                        "column": 4
                    },
                    "end": {
                        "line": 25,
                        "column": 5
                    }
                },
                "11": {
                    "start": {
                        "line": 24,
                        "column": 8
                    },
                    "end": {
                        "line": 24,
                        "column": 50
                    }
                },
                "12": {
                    "start": {
                        "line": 27,
                        "column": 4
                    },
                    "end": {
                        "line": 27,
                        "column": 19
                    }
                },
                "13": {
                    "start": {
                        "line": 36,
                        "column": 25
                    },
                    "end": {
                        "line": 36,
                        "column": 78
                    }
                },
                "14": {
                    "start": {
                        "line": 37,
                        "column": 19
                    },
                    "end": {
                        "line": 40,
                        "column": 10
                    }
                },
                "15": {
                    "start": {
                        "line": 38,
                        "column": 8
                    },
                    "end": {
                        "line": 38,
                        "column": 31
                    }
                },
                "16": {
                    "start": {
                        "line": 39,
                        "column": 8
                    },
                    "end": {
                        "line": 39,
                        "column": 19
                    }
                },
                "17": {
                    "start": {
                        "line": 42,
                        "column": 4
                    },
                    "end": {
                        "line": 44,
                        "column": 5
                    }
                },
                "18": {
                    "start": {
                        "line": 43,
                        "column": 8
                    },
                    "end": {
                        "line": 43,
                        "column": 29
                    }
                },
                "19": {
                    "start": {
                        "line": 46,
                        "column": 4
                    },
                    "end": {
                        "line": 48,
                        "column": 5
                    }
                },
                "20": {
                    "start": {
                        "line": 47,
                        "column": 8
                    },
                    "end": {
                        "line": 47,
                        "column": 35
                    }
                },
                "21": {
                    "start": {
                        "line": 50,
                        "column": 4
                    },
                    "end": {
                        "line": 50,
                        "column": 18
                    }
                }
            },
            "s": {
                "0": 43,
                "1": 43,
                "2": 43,
                "3": 43,
                "4": 43,
                "5": 0,
                "6": 0,
                "7": 43,
                "8": 43,
                "9": 43,
                "10": 43,
                "11": 43,
                "12": 43,
                "13": 2184,
                "14": 2184,
                "15": 6552,
                "16": 6552,
                "17": 2184,
                "18": 2184,
                "19": 0,
                "20": 0,
                "21": 0
            },
            "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9",
            "hash": "26596d813cde8cfd5d6449001d4e49f4283c164a"
        }
    }
    
    

    以上的数据我们省掉了 fnMap以及branchMap的数据。

    而我们的的codeDiff的数据:

    image

    说明改动的行数只是49-50行

    所以处理过的coverage的结果数据为

    {
        "s": {
            "21": 0
        },
        "hash": "26596d813cde8cfd5d6449001d4e49f4283c164a",
        "path": "/app/thirdCode/c94933a0-7250-11eb-8c93-c9620716ef34_1/xxx/client/src/utils/location.js",
        "statementMap": {
            "21": {
                "end": {
                    "line": 50,
                    "column": 18
                },
                "start": {
                    "line": 50,
                    "column": 4
                }
            }
        },
        "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9"
    }
    

    相应的fn, branch也是相应的处理。

    不足:

    上述的方式其实存在一个问题,前端的增量覆盖计算的逻辑并不是跟java的增量的逻辑一致的,java的最小增量的计算单位是方法,而前端的最小增量单位是语句。所以并不能很好的得到结果是前端某个代码改动后,需要覆盖这个代码所在的方法的内容,而只是需要覆盖到改动的语句就可以了。

    展开全文
  • JaCoCo 是一个非常常用的计算代码覆盖率的工具. 达到的效果就是可以分析出在代码启动到某个时间点那些代码是执行过的, 哪些代码是从没执行的, 从而了解到代码测试的覆盖程度. 支持类级别, 方法级别, 行级别的覆盖率...

    官网地址: http://www.eclemma.org/jacoco/

    JaCoCo 是一个非常常用的计算代码覆盖率的工具. 达到的效果就是可以分析出在代码启动到某个时间点那些代码是执行过的, 哪些代码是从没执行的, 从而了解到代码测试的覆盖程度.
    支持类级别, 方法级别, 行级别的覆盖率统计. 同时也支持分支级别的统计.

    下图是官网的截图, 绿色代表已执行, 红色代表未执行, 黄色代表执行了一部分, 下方还有在一个类, 一个包的覆盖率的比例. 非常直观了.
    image.png

    实现原理

    如果我们接到这个需求我们会怎么实现呢? 一种最简单的方式就是在每行代码上面都做一个标记, 标记这行代码是否被执行, 如果这个标记被执行了, 证明下行代码将会被执行. 其实JaCoCo的原理也差不多是如此. 至于这个标记是在哪里插入的, 插入了什么, 如何根据标记计算覆盖率等问题就是本文重点.

    JaCoCo如何修改代码

    JaCoCo的修改代码的方式有两种

    • 一种是on-the-fly, 也就是实时修改代码, 原理是使用java agent技术, 是这次着重介绍的.
    • 一种是offline , 也就是由于特殊原因导致无法使用on-the-fly, 例如环境不支持使用java agent等原因.

    JaCoCo插入了什么?

    下面是一个例子. 针对下面的代码, JaCoCo做了什么呢, 我们来根据JaCoCo修改后的字节码再进行反编译, 看看修改了什么

    public class JacocoTest {
    
        public static void main(String[] args) {
            int a = 10;
            a = a+20;
            System.out.println();
            if (a > 10) {
                test1();
            } else {
                test2();
            }
            System.out.println();
        }
    
        public static void test1() {
            System.out.println("");
        }
    
        public static void test2() {
            System.out.println("");
            throw new RuntimeException("");
        }
    }

    JaCoCo加工后的代码可通过修改JaCoCo源码输出修改后文件, 并通过反编译工具如 CFR 进行反编译得到, 如下:

    public class JacocoTest {
        private static transient /* synthetic */ boolean[] $jacocoData;
    
        public JacocoTest() {
            boolean[] arrbl = JacocoTest.$jacocoInit();
            arrbl[0] = true;
        }
    
        public static void main(String[] arrstring) {
            boolean[] arrbl = JacocoTest.$jacocoInit();
            int a = 10;
            ++a;
            arrbl[1] = true;
            System.out.println();
            if (++a > 10) {
                arrbl[2] = true;
                JacocoTest.test1();
                arrbl[3] = true;
            } else {
                JacocoTest.test2();
                arrbl[4] = true;
            }
            System.out.println();
            arrbl[5] = true;
        }
    
        public static void test1() {
            boolean[] arrbl = JacocoTest.$jacocoInit();
            System.out.println("");
            arrbl[6] = true;
        }
    
        public static void test2() {
            boolean[] arrbl = JacocoTest.$jacocoInit();
            System.out.println("");
            arrbl[7] = true;
            arrbl[8] = true;
            throw new RuntimeException("");
        }
    
        private static /* synthetic */ boolean[] $jacocoInit() {
            boolean[] arrbl = $jacocoData;
            boolean[] arrbl2 = arrbl;
            if (arrbl != null) return arrbl2;
            Object[] arrobject = new Object[]{4473305039327547984L, "com/xin/test/JacocoTest", 9};
            UnknownError.$jacocoAccess.equals(arrobject);
            arrbl2 = $jacocoData = (boolean[])arrobject[0];
            return arrbl2;
        }
    }
    
    

    一目了然, JaCoCo的操作和预测的是差不多的, 标记是使用了一个boolean数组, 只要执行过对应的路径就对boolean数组进行赋值, 最后对boolean进行统计即可得出覆盖率. 这个标记官方有个名字叫探针 (Probe)

    但有个问题: 为什么不是所有执行语句后面都有一个探针呢?
    这个涉及到探针的插入策略的问题, 官方文档有介绍, 本文也会介绍到.

    探针插入策略

    怎么插入探针可以统计覆盖率的吗?
    对于插入策略可分为下面三个问题

    • 如何统计某个方法是否被触发
    • 如何统计不同分支的执行情况
    • 如果统计执行的代码块的执行情况

    方法是否被触发

    这个比较容易处理, 只需要在方法头或者方法尾加就行了.

    • 方法尾加:
      这种处理比较麻烦, 可能有多个return或者throw.能说明方法被执行过, 且说明了探针上面的方法被执行了, 同时也说明了下个语句准备.
    • 方法头加: 处理很简单, 但只能说明方法有进去过.

    探针上面是否被执行很重要, 因此JaCoCo选择在方法结尾处统计.

    不同分支的执行情况

    不同的分支指遇到了例如if判断语句, for判断语句, while, switch等, 会跳到不同代码块执行, 中间可能会漏执行部分代码. 因为jacoco是针对字节码工作的, 因此这类跳转指令对应的字节码为 GOTOIFxTABLESWITCH or LOOKUPSWITCH, 统称为JUMP类型

    这种JUMP类型也有两种不同的情况, 一种是不需要条件jump, 一种是有条件jump

    • 无条件jump (goto), 这种一般出现在continue, break 中, 直接跳转.这种不需要覆盖不跳转的分支和跳转语句. jacoco会在jump之前加个探针, 其实和上面对"方法进行触发"的原理比较接近, 可以看成是 goto 是方法的结尾.

    image.png

    • 有条件jump (ifxx), 这种经常出现于if等有条件的跳转语句. 这种一般会存在两个分支需要覆盖. 一个常见的if分支字节码的流程大概是这样子的, 因为字节码是顺序执行的, 所以还需要 goto 的帮助.
    function() {
        指令1
        if (){
           指令3
        } else {
           指令4
        }
        指令5
    }

    image.png

    下图是探针插入的情况, 探针1和探针2分别在不同的地方
    image.png

    其实条件分支还有另一种特殊的情况如下. 特殊在于没有else, 指令3 可执行可不执行. 但就算条件为false, 也是一条路径需要进行统计的. 但因为条件为false直接跳转到探针5了, 因此加了探针2后蓝色路径需要加上goto跳过探针2. 这种实际处理起来会比较麻烦.

    function() {
        指令1
        if (条件){
           指令3
        }
        指令5
    }

    image.png

    JaCoCo用了一种更好的方案去加探针2. 那就是翻转条件, 把 if 改成 ifnot . 不影响代码逻辑, 但加探针和goto都非常方便.
    image.png

    统计执行的代码块的执行情况

    这个比较简单, 只要在每行代码前都插入探针即可, 但这样会有个问题. 也就是性能问题, 需要插入大量的探针. 那有没有办法优化一下呢?
    如果几行代码都是顺序执行的, 那只要在代码段前, 代码段后放置探针即可. 但还会有问题, 某行代码抛异常了怎么办?
    JaCoCo考虑到非方法调用的指令一般出现异常的概率比较低. 因此对非方法调用的指令不插入探针, 而对每个方法调用之前都插入探针.
    这当然会存在问题, 例如 NullPointerExceptionorArrayIndexOutOfBoundsException 异常出现会导致在临近的非方法调用的指令的覆盖率会有异常.

    下图是在 a/0抛出了异常, 但除了test1()上面的探针能捕获 int a = 10; 这个语句之外其他都无法判定是否执行.
    image.png

    image.png

    JaCoCo代码层面如何实现

    主要使用了asm进行类的修改, 需要有些asm的知识储备

    对代码的修改点

    看了上面的反编译后的例子, 可以看到具体改了3个地方.

    1. 类增加了$jacocoData属性
    2. 每个方法开头都增加了一个boolean数组的局部变量, 并调用$jacocoInit进行赋值
    3. 类增加了$jacocoInit方法
    4. 对方法里面的语句进行boolean数组里面元素的修改.

    代码修改涉及到的类介绍

    实现类的修改主要集中在下面几个类 (交互图只是突出重点的类, 省略的很多细节)

    image.png

    CoverageTransformer: 就是连接java agent的类, 继承了 java.lang.instrument.ClassFileTransformer, 是java agent的典型使用.

    Instrumenter: 类似于一个门面, 提供类修改的方法, 没有太多具体实现的逻辑. 输出jacoco修改后的文件也是改了这个类的代码.

    IProbeArrayStrategy: 是boolean数组的生成策略类. 用于实现上面1 $jacocoData属性,2 (增加boolean数组并赋值) 和3 \$jacocoInit方法. 因为设计到class的处理和method的处理, 因此在这两者的处理类里面都能看到他的身影.

    由于针对不同的情况,如class的jdk版本号, 是否是接口还是普通类, 是否是内部类等生成不同属性和方法, 因此有不同的实现, 由下面的 ProbeArrayStrategyFactory 工厂进行创建.

    ProbeArrayStrategyFactory: 是一个工厂, 负责生成IProbeArrayStrategy.

    image.png

    后面还有一部分类, 是插入探针的重点类
    image.png

    ClassProbesAdapter: 这个看名字就知道是个适配器, 没有太多的逻辑. 个人感觉这里的设计有点不合理.
    原因是: 适配器模式更适合那些调用类和被调用类两者没什么联系, 只能通过依赖调用被调用类, 但又想解耦被调用类, 因此弄了一个适配器作为中间人屏蔽调用类对被调用类的依赖. 但ClassProbesAdapter 和 被调用类 本来就同父的, 都是依赖ClassVisitor, 只是处理内部类和普通类上面有一些区别, 适配器也没有什么自己特有的流程. 因此使用模板模式更合适, 可读性也更好一些.

    ClassInstrumenter: 这个就是上面提到的ClassProbesAdapter的代理的类了, 具体处理逻辑在这里, 其实也没有太多的逻辑, 因为IProbeArrayStrategy 已经把类级别的事情做了,ClassInstrumenter 调用一下就可以了. 并且还要创建方法处理器.
    ClassInstrumenter 其实是一个具体实现, 继承 ClassProbesVisitor, 还有另一个实现是 ProbeCounter 作用是统计所有探针的数量, 但不做任何处理, 在ProbeArrayStrategyFactory 里面负责统计完之后生成不同的实现类. 例如探针数为0, 则用NoneProbeArrayStategy即可.

    MethodProbesAdapter: 也是一个适配器, 作用是找到那些指令需要插入探针的, 再调用MethodInstrumenter来插入.

    MethodInstrumenter: 这个是解决如何插探针的问题. 大部分情况可能直接插入就可以了, 但少部分情况需要做些额外处理才能插入.

    ProbeInserter: 这个负责生成插入探针的代码, 例如 插入 arrbl[2] = true; 且因为在方法头增加了一个局部变量, 因此还要处理一些class文件修改层面的事情, 例如剩余代码对局部变量的引用都要+1, StackSize 等都要进行修改. 这个需要了解class文件的格式和字节码一些基础知识.

    对方法插入具体的实现

    针对上文说到的探针插入策略, 主要介绍就几个点的实现:

    1. 方法尾插入探针
    2. goto 前插入探针, ifxx 后插入探针 (都属于跳转就放一齐了)
    3. 在方法调用前插入探针, 非方法调用不插入探针.

    方法尾插入探针

    在字节码级别有两个指令是说明到了方法尾的, 那就是 xRETURN or THROW. 是最简单的插入方式.

    MethodProbesAdapter
    @Override
        public void visitInsn(final int opcode) {
            switch (opcode) {
            case Opcodes.IRETURN:
            case Opcodes.LRETURN:
            case Opcodes.FRETURN:
            case Opcodes.DRETURN:
            case Opcodes.ARETURN:
            case Opcodes.RETURN:
            case Opcodes.ATHROW:
                probesVisitor.visitInsnWithProbe(opcode, idGenerator.nextId());
                break;
            default:
                probesVisitor.visitInsn(opcode);
                break;
            }
        }
    MethodInstrumenter
        @Override
        public void visitInsnWithProbe(final int opcode, final int probeId) {
            probeInserter.insertProbe(probeId);
            mv.visitInsn(opcode);
        }

    goto 前插入探针, ifxx 后插入探针

    MethodProbesAdapter
    @Override
        public void visitJumpInsn(final int opcode, final Label label) {
            if (LabelInfo.isMultiTarget(label)) {
                probesVisitor.visitJumpInsnWithProbe(opcode, label,
                        idGenerator.nextId(), frame(jumpPopCount(opcode)));
            } else {
                probesVisitor.visitJumpInsn(opcode, label);
            }
        }

    LabelInfo.isMultiTarget(label) 这个方法有点特殊, 也说明了不是所有的 jump 都需要加的探针的. 也算是一个小优化吧.
    在处理方法前会对方法进行一个控制流分析, 具体逻辑在org.jacoco.agent.rt.internal_43f5073.core.internal.flow.LabelFlowAnalyzer
    只有对于一些有可能从多个路径到达的指令(包括正常的顺序执行或者jump跳转)才会需要加探针. 有时候编译器会做一些优化, 导致新增了goto, 例如 一个执行

    boolean b = a > 10;

    编译出来的代码是

             L6 {
                 iload1
                 bipush 10
                 if_icmple L7
                 iconst_1 //推1 到栈帧
                 goto L8
             }
             L7 {
                 iconst_0 //推0 到栈帧
             }
             L8 {
                 istore2 //栈帧出栈并把值保存在变量中
             }
    

    goto L8 这个goto加探针就没什么意义, 因为L8段只来自于此指令, 不会从别的地方过来了. 加探针是为了区分不同分支. 但goto L8 到L8段并没有分支. 因此没必要加探针了. 当然也不是所有goto都不用加探针. 加入L8段有其他路径可以过来, 那就有必要是从哪个分支过来的. 这个其实也是JaCoCo统计的一个点, 分支的执行情况而不仅仅是代码覆盖率. 我可以把代码都覆盖了, 但不一定把分支都覆盖了.

    MethodInstrumenter
        @Override
        public void visitJumpInsnWithProbe(final int opcode, final Label label,
                final int probeId, final IFrame frame) {
            if (opcode == Opcodes.GOTO) {
                //如果是goto则在goto前插入
                probeInserter.insertProbe(probeId);
                mv.visitJumpInsn(Opcodes.GOTO, label);
            } else {
               //如果是其他跳转语句则需要翻转if 且加入探针和goto.
                final Label intermediate = new Label();
                mv.visitJumpInsn(getInverted(opcode), intermediate);
                probeInserter.insertProbe(probeId);
                mv.visitJumpInsn(Opcodes.GOTO, label);
                mv.visitLabel(intermediate);
                frame.accept(mv);
            }
        }

    在方法调用前插入探针, 非方法调用不插入探针

    同样经过LabelFlowAnalyzer分析之后标记了哪个指令段是方法调用的

    LabelFlowAnalyzer
        @Override
        public void visitInvokeDynamicInsn(final String name, final String desc,
                final Handle bsm, final Object... bsmArgs) {
            successor = true;
            first = false;
            markMethodInvocationLine();
        }
    
        private void markMethodInvocationLine() {
            if (lineStart != null) {
                LabelInfo.setMethodInvocationLine(lineStart);
            }
        }

    只要知道做了标记, 就很容易做处理了.

    MethodProbesAdapter
        @Override
        public void visitLabel(final Label label) {
            if (LabelInfo.needsProbe(label)) {
                if (tryCatchProbeLabels.containsKey(label)) {
                    probesVisitor.visitLabel(tryCatchProbeLabels.get(label));
                }
                probesVisitor.visitProbe(idGenerator.nextId());
            }
            probesVisitor.visitLabel(label);
        }
    LabelInfo
        public static boolean needsProbe(final Label label) {
            final LabelInfo info = get(label);
            return info != null && info.successor
                    && (info.multiTarget || info.methodInvocationLine);
        }

    对实现只分析了一部分比较核心的, 还有对trycatch, switch等的处理可自己去探索.

    性能影响

    JaCoCo文档有介绍

    The control flow analysis and probe insertion strategy described in this document allows to efficiently record instruction and branch coverage. In total classes instrumented with JaCoCo increase their size by about 30%. Due to the fact that probe execution does not require any method calls, only local instructions, the observed execution time overhead for instrumented applications typically is less than 10%.

    文中提到的控制流分析和探针的插入策略能高效的记录指令和分支的覆盖情况. 在所有类都被JaCoCo注入的情况下大小大概会增加30%, 由于探针执行并不需要任何方法调用, 只是执行本地的指令, 因此被注入的应用执行时间开销一般会小于10%.

     

    转自:https://segmentfault.com/a/1190000022259363  感谢原作者

    展开全文
  • 代码覆盖率常被用来作为衡量单元测试好坏的指标,于是测试人员费尽心思设计案例覆盖代码。用代码覆盖率来衡量,有利也有有弊。本文我们就代码覆盖率展开讨论。1. 语句覆盖语句覆盖又称行覆盖,段覆盖,基本块覆盖,...
  • 测试用例的覆盖率如何统计呢,如何知道开发的代码,我们都测到了,不会存在漏测的情况。pytest-cov先命令行安装 pytest-cov 2.10.1版本pip install pytest-cov==2.10.1环境要求:1.python3.6.6 版本备注:其它版本没...
  • 1. 代码覆盖率单元测试代码覆盖率作为一种度量方式,可以计算单元测试用例对于被测代码的覆盖程度,即:被执行的代码数量和代码总数量的比值统计代码覆盖率,经常在单元测试后再进行,可以为测试结果提供评判依据...
  • Diff coverage是JaCoCo扩展,它基于提供的计算新/修改后代码的代码覆盖率。 diff内容可以通过补丁文件的路径,URL或使用嵌入式git(请参阅)提供。 我为什么要使用它? 强制每个开发人员对其自己的代码质量负责...
  • 代码覆盖率

    2015-05-01 15:42:39
    首先,我们需要明确一个问题,什么是代码覆盖率?基本上可以理解为测试过程中运行代码的行数与总行数的比率。代码覆盖程度的度量方式有很多种,比如语句覆盖,判定覆盖,条件覆盖,路经覆盖,这四种基本的覆盖方式...
  • 1.在“代码覆盖率”选中当前要测试的组件 2.调试某个测试 3.点击“停止”以停止调试 4.然后再重新运行测试 这时候就会提示如下错误: 代码覆盖率收集错误: Visual Studio 性能和覆盖率记录引擎已在计算机上运行...
  • 工作需要,领导让统计单元测试的代码覆盖率,试了试coverage.py库,发现测试代码和被测代码好像必须在同一目录下才行,如果不在同一目录就会找不到模块的错误,我在fileb目录下执行了coverage run test_mypath.py,...
  • 测试用例的覆盖率如何统计呢,如何知道开发的代码,我们都测到了,不会存在漏测的情况。pytest-cov先命令行安装 pytest-cov 2.10.1版本pip install pytest-cov==2.10.1环境要求:1.python3.6.6 版本备注:其它版本没...
  • 聊聊 Python 代码覆盖率工具 - Coverage

    万次阅读 2019-07-28 00:20:20
    单元测试代码覆盖率作为一种度量方式,可以计算单元测试用例对于被测代码的覆盖程度,即:被执行的代码数量和代码总数量的比值、统计代码覆盖率;经常在单元测试后再进行,Python 项目最常使用的代码覆盖率统计工具...
  • 新增代码覆盖率解析

    2018-11-21 10:52:00
    大致原理步骤: 1)编译阶段:加入编译选项 –fprofile-arcs –ftest-coverage。会在目标代码文件中加入对应的钩子,采集执行的统计来实现覆盖...4)结合git diff生成的diff文件,计算新增代码覆盖率。 [UT/F...
  • Android手工测试的代码覆盖率

    千次阅读 2015-05-07 20:29:36
    昨天我们探究了UI自动化中代码覆盖率,今天我们来看看如何实现功能测试人员测试过程中,代码覆盖率计算 先纠个错昨天我们使用jacoco,在build.gradle加入了很多代码:apply plugin: 'jacoco' .... jacoco{ tool...
  • 在本教程中,我们将向您展示如何使用Maven为您的项目生成Cobertura代码覆盖率报告。 1. Cobertura代码覆盖率报告 不执行任何操作,只需键入以下Maven命令即可自动下载并运行maven-cobertura-plugin 。 c:\...
  • <p>I am writing unit tests for my golang code, and there are a couple methods that I would like to be ignored when coverage is calculated. Is this possible? If so, how? </div>
  • 摘要:https://www.cnblogs.com/yoyoketang/p/13672409.html
  • 代码覆盖率是衡量软件测试完成情况的指标,通常基于测试过程中已检查的程序源代码比例计算得出。代码覆盖率可以有效避免包含未测试代码的程序被发布。1. 问题背景代码覆盖(Code coverage)是软件测试中的一种度量,...
  • 昨天我们探究了UI自动化中代码覆盖率,今天我们来看看如何实现功能测试人员测试过程中,代码覆盖率计算先纠个错昨天我们使用jacoco,在build.gradle加入了很多代码:apply plugin: 'jacoco' .... jacoco{ tool...
  • 单元测试代码覆盖率作为一种度量方式,可以计算单元测试用例对于被测代码的覆盖程度,即:被执行的代码数量和代码总数量的比值 统计代码覆盖率,经常在单元测试后再进行,可以为测试结果提供评判依据 Python 项目...
  • 代码覆盖率是衡量软件测试完成情况的指标,通常基于测试过程中已检查的程序源代码比例计算得出。代码覆盖率可以有效避免包含未测试代码的程序被发布。代码覆盖率能不能提高软件的可靠性?答案是肯定的,代码的覆盖率...
  • 1. 代码覆盖率单元测试代码覆盖率作为一种度量方式,可以计算单元测试用例对于被测代码的覆盖程度,即:被执行的代码数量和代码总数量的比值统计代码覆盖率,经常在单元测试后再进行,可以为测试结果提供评判依据...

空空如也

空空如也

1 2 3 4 5 ... 15
收藏数 296
精华内容 118
关键字:

代码覆盖率计算