命令行模式覆盖率统计
功能简介
cjcov(Cangjie Coverage)是仓颉语言的官方覆盖率统计工具,用于生成仓颉语言程序的覆盖率报告。
使用说明
通过 cjcov -h 即可查看命令使用方法,如下所示。由几个板块组成,从上到下分别是:当前命令使用形式(Usage)、当前命令用途、支持的可用参数(Options)。
Usage: cjcov [options]
A tool used to summarize the coverage in html reports.
Options:
  -v, --version                 Print the version number, then exit.
  -h, --help                    Show this help message, then exit.
  -r ROOT, --root=ROOT          The root directories of your source files, defaults to '.', the current directory.
                                File names are reported relative to this root.
  -o OUTPUT, --output=OUTPUT    The output directories of html reports, defaults to '.', the current directory.
  -b, --branches                Report the branch coverage. (It is an experimental feature and may generate imprecise branch coverage.)
  --verbose                     Print some detail messages, including parsing data for the gcov file.
  --html-details                Generate html reports for each source file.
  -x, --xml                     Generate a xml report.
  -j, --json                    Generate a json report.
  -k, --keep                    Keep gcov files after processing.
  -s SOURCE, --source=SOURCE    The directories of cangjie source files.
  -e EXCLUDE, --exclude=EXCLUDE
                                The cangjie source files starts with EXCLUDE will not be showed in coverage reports.
  -i INCLUDE, --include=INCLUDE
                                The cangjie source files starts with INCLUDE will be showed in coverage reports.
基本的命令使用方法如下所示,cjcov 为主程序名称,--version 表示为显示 cjcov 的版本号。部分配置项支持长短选项两种写法,效果相同,具体可以使用 cjcov --help 命令参考用法。
cjcov -version 或者 cjcov -v
使用步骤
仓颉版本包准备 --> 仓颉源码准备 -->  使用 --coverage 编译选项构建仓颉源码,生成二进制文件 --> 执行二进制文件 --> cjcov 生成覆盖率统计结果
下面举一个 hello world 的覆盖率的例子(假设当前目录是 WORKPATH):
- 
仓颉版本包准备
假设仓颉版本包解压在
WORKPATH目录下,则执行source WORKPATH/cangjie/envsetup.sh命令即可。 - 
仓颉源码准备
源码目录结构如下:
src/ └── main.cjmain.cj源码内容如下:main(): Int64 { print("hello world\n") return 0 } - 
编译源码,该例子用
cjpm编译举例在
WORKPATH目录下执行以下命令:cjpm init --name test cjpm build --coverage编译完成之后在
WORKPATH目录下会生成default.gcno文件。 - 
运行编译出来的二进制
在
WORKPATH目录下执行cjpm run --skip-build命令,运行完成之后WORKPATH目录下会生成default.gcda文件。 - 
cjcov生成html在
WORKPATH目录执行cjcov -o output --html-details,更多cjcov参数使用可参考命令说明章节。 
执行完 cjcov 命令之后,在 WORKPATH/output 目录会有以下文件:
output
├── cjcov_logs (该目录存放一些 cjcov 执行过程的详细日志,可不用关注)
│   ├── cjcov.log
│   └── gcov_parse.log
├── index.html (总的覆盖率报告,通过浏览器打开)
└── src_main.cj.html (单个文件的覆盖率,可以通过打开 index.html 自动跳转到该文件)
命令说明
cjcov -h | --help
显示 cjcov 基本使用方法。
cjcov -v | --version
显示 cjcov 的版本号,只要指定了 -v 或者 --version 参数,不管输入其他任何选项参数都不生效,只会显示版本号。如 --version 和 --help 同时使用,则显示 version 信息后退出。
cjcov --verbose
指定该选项后会将一些日志信息生成到 cjcov_logs 目录中,该参数默认不生效,即默认不会打印中间信息。gcov 文件是 cjcov 工具生成的中间文件,cjcov 解析 gcov 文件的格式如下:
==================== start: main.cj.gcov =====================
noncode line numbers:
[0, 0, 0, 0, 1, 2, 6, 7, 9, 10, 11, 15, 17, 18]
uncovered line numbers:
[5]
covered data:
[(16, 1), (3, 1), (4, 1), (8, 1), (12, 1), (13, 1), (14, 1)]
branches data:
line number:    4  ==>  data: [(0, 0), (1, 1)]
===================== end: main.cj.gcov =======================
指定该选项参数,会显示每个 gcov 文件的详细覆盖率数据。
具体字段解释如下:
start: xxx.gcov, end: xxx.gcov:两行中间的文本是对应xxx.gcov文件解析到的覆盖率数据。noncode line numbers:显示的是不统计到总代码行的行号,在html中是以白色底呈现,对应gcov中的以-开头的行数。uncovered line numbers:显示的是没有覆盖到的数据,在html中是以红色底呈现,对应gcov文件中以#####开头的行数。covered data:显示的是覆盖到的数据,以(代码行数, 覆盖次数)呈现,在对应html中以绿色呈现,只要覆盖次数大于 0,在html中的Exec一列中显示为Y,对应于gcov文件以数字开头的行数。branches data:显示的分支覆盖数据,以(代码行数, 分支覆盖次数)呈现,在对应html中的Branch一列中,有一个倒三角形,显示的是分支覆盖数/总分支数。该数据对应于gcov文件中以branch开头的数据。
cjcov --html-details
如果指定该参数,表示会生成仓颉文件对应的 html。在总的 index 文件里面会有每个子 html 的索引。子 html 文件和 index.html 放在同一个目录。
子 html 文件名是由目录和文件名由下划线拼接起来。如源文件是 src/main.cj,生成的 html 名字为 src_main.cj.html。如果源文件路径带有特殊字符会被替换成 =,下文文件名包含特殊字符章节会有更详细的描述。
如果没有指定该参数,表示不会生成子 html 。在总的 index 文件里面会显示每个子 html 的覆盖率数据,但是不能跳转到对应的子 html 文件。
该参数默认不生效。即默认只会生成一个 index.html, 不会生成子 html 文件。
cjcov -x | --xml
如果指定该参数,则会在指定输出路径生成 coverage.xml 文件,coverage.xml 记录的是所有文件的覆盖率数据。
cjcov -j | --json
如果指定该参数,则会在指定输出路径生成 coverage.json 文件,coverage.json 记录的是所有文件的覆盖率数据。
cjcov -k | --keep
指定该参数后则不会删除生成的 gcov 中间文件。如果 gcov 文件不删除,会造成执行次数的累加,可能会影响覆盖率数据的准确性。
默认该参数不生效,即默认会删除 gcov 中间文件。
cjcov -b | --branches
指定该参数后则会生成分支覆盖率信息。
默认该参数不生效,即默认不生成分支的覆盖率信息,此时在 html 报告中的分支覆盖率数据百分比显示为 -。
cjcov -r ROOT | --root=ROOT
该参数指定的 ROOT 参数,表示在 ROOT 目录或者在其递归子目录能找到 gcda 文件,gcda 和 gcno 文件默认会生成在一起,建议不要手动特意去把 gcda 文件和 gcno 文件分开存放,不然可能会发生程序不能运行的情况。
参数指定的 ROOT 目录如果不存在,cjcov 工具会有报错提示。
不指定该参数,默认会以当前目录为 ROOT 目录。
cjcov -o OUTPUT | --output=OUTPUT
该参数指定的 OUTPUT 参数,表示 html 覆盖率报告的输出路径。
如果该 OUTPUT 目录不存在,而且其父目录也不存在,cjcov 工具会有报错提示;如果 OUTPUT 目录不存在,但其父目录存在,cjcov 会帮助创建 OUTPUT 目录。
不指定该参数,默认会以当前目录为 OUTPUT 目录来存放 html 文件。
-s SOURCE | --source=SOURCE
该参数指定的 SOURCE 参数,表示仓颉源文件的代码路径,html 总覆盖率报告 index.html 会有各个源文件的索引,这些文件路径记录的是一个相对路径。如果指定 -s SOURCE |--source SOURCE 参数,优先以 SOURCE 路径列表中的路径作为相对路径的参考路径,如果没有指定该参数,则以 -r ROOT | --root=ROOT 作为相对路径的参考路径,如果都没有指定,则以当前路径作为相对路径的参考路径。
示例:
仓颉代码目录结构如下:
/work/cangjie/tests/API/test01/src/1.cj
/work/cangjie/tests/API/test01/src/2.cj
/work/cangjie/tests/LLVM/test02/src/3.cj
/work/cangjie-tools/tests/LLVM/test01/src/4.cj
/work/cangjie-tools/tests/LLVM/test02/src/5.cj
- 
在
/work目录执行命令:cjcov --root=./ -s "/work/cangjie /work/cangjie-tools/tests" --html-details --output=html_output最后 html 中呈现的源文件相对路径是:
tests/API/test01/src/1.cj tests/API/test01/src/2.cj tests/LLVM/test02/src/3.cj LLVM/test01/src/4.cj LLVM/test02/src/5.cj - 
在
/work目录执行命令, 没有指定--root参数和--source参数,默认当前所在路径为相对路径的参考路径,执行命令如下:cjcov --html-details --output=html_output最后 html 中呈现的源文件相对路径是:
cangjie/tests/API/test01/src/1.cj cangjie/tests/API/test01/src/2.cj cangjie/tests/LLVM/test02/src/3.cj cangjie-tools/tests/LLVM/test01/src/4.cj cangjie-tools/tests/LLVM/test02/src/5.cj 
-e EXCLUDE | --exclude=EXCLUDE
该参数指定的 EXCLUDE 参数,表示不需要生成覆盖率信息的源文件列表,支持指定目录和文件。
示例:
仓颉代码目录结构如下:
/usr1/cangjie/tests/API/test01/src/1.cj
/usr1/cangjie/tests/API/test01/src/2.cj
/usr1/cangjie/tests/LLVM/test02/src/3.cj
/usr1/cangjie-tools/tests/LLVM/test01/src/4.cj
/usr1/cangjie-tools/tests/LLVM/test02/src/5.cj
在 /usr1 目录执行命令:
cjcov --root=./ -s "/usr1/cangjie" -e "/usr1/cangjie-tools/tests/LLVM" --html-details --output=html_output
最后 html 中呈现的源文件相对路径是,其中以 /usr1/cangjie-tools/tests/LLVM 路径开头的文件不会出现在 html 的文件列表中。
tests/API/test01/src/1.cj
tests/API/test01/src/2.cj
tests/LLVM/test02/src/3.cj
-i INCLUDE | --include=INCLUDE
该参数指定的 INCLUDE 参数,表示以 INCLUDE 开头的文件会显示在 index.html 的文件列表中,支持指定目录和文件。如果 -e | --exclude 和 -i | --include 指定的参数有路径重复,会有报错提示。
示例:
仓颉代码目录 /usr1/cangjie/tests 结构如下:
├── API
│   └── test01
│       └── src
│           ├── 1.cj
│           └── 2.cj
└── LLVM
    └── test02
        └── src
            └── 3.cj
在 /usr1 目录执行命令, 其中 -i 参数表示需要体现在覆盖率报告 index.html 的文件,命令如下:
cjcov --root=./ -s "/usr1/cangjie" -i "/usr1/cangjie/tests/API/test01/src/1.cj /usr1/cangjie/tests/LLVM/test02" --html-details --output=html_output
上面命令执行后, 在 index.html 中文件路径列表如下(tests/API/test01/src/2.cj 不在 -i 参数指定的列表里面,所以不会出现在 html 的文件列表中):
tests/API/test01/src/1.cj
tests/LLVM/test02/src/3.cj
特殊场景
二进制无法正常执行结束
对于常驻的网络服务程序无法正常结束二进制文件并生成 gcda 覆盖率数据的场景,需要手动执行退出脚本生成 gcda 覆盖率数据。
1)将以下脚本内容保存为 stop.sh(此脚本执行依赖 gdb)
#!/bin/sh
SERVER_NAME=$1
pid=`ps -ef | grep $SERVER_NAME | grep -v "grep" | awk '{print $2}'`
echo $pid
gdb -q attach $pid <<__EOF__
p exit(0)
__EOF__
2)常驻服务程序完成业务逻辑操作覆盖后,执行 stop.sh {service_name},如通过 ./main 启动常驻服务进程,通过如下方式停止进程产生 gcda 数据
sh stop.sh ./main
文件名包含特殊字符
建议遵循仓颉编程规范命名文件,不建议包含除 [0-9a-zA-Z_] 之外的字符,特殊字符会被替换成 =。
如果文件名有特殊字符,为保证 html 跳转正确,index.html 中呈现的 html 名字和 html 本身文件名会不一致,html 文件名的特殊字符都会被替换成 =。
示例如下:
代码结构:
src
├── 1file#.cj
├── file10_abc.cj
├── file11_.aaa-bbb.cj
├── file12!#aaa!bbb.cj
├── file13~####.cj
├── file14*aa.cj
├── file15`.cj
├── file16(#).cj
├── file2;aa.cj
├── file3,?.cj
├── file4@###.cj
├── file5&cc.cj
├── file6=.cj
├── file7+=.cj
├── file8$.cj
├── file9-aaa.cj
└── main.cj
生成 html 文件名,其中除了 [0-9a-zA-Z_.=] 之外,其他特殊字符都被替换成了 '=':
.
├── index.html
├── src_1file=.cj.html
├── src_file10_abc.cj.html
├── src_file11_.aaa=bbb.cj.html
├── src_file12==aaa=bbb.cj.html
├── src_file13=####.cj.html
├── src_file14=aa.cj.html
├── src_file15=.cj.html
├── src_file16===.cj.html
├── src_file2=aa.cj.html
├── src_file3==.cj.html
├── src_file4=###.cj.html
├── src_file5=cc.cj.html
├── src_file6=.cj.html
├── src_file7==.cj.html
├── src_file8=.cj.html
├── src_file9=aaa.cj.html
└── src_main.cj.html
分支覆盖率功能
分支覆盖率是一个试验阶段的功能,会出现分支覆盖率数据不准确的情况。
目前已知会出现分支覆盖率数据不准确的场景包含以下几种表达式:
- 
try-catch-finally表达式 - 
循环表达式(包括
for表达式、while表达式) - 
if-else表达式 
部分代码未记录到行覆盖率数据中
部分代码不会记录到行覆盖率数据中,属于正常情况。整体而言,如果一行代码仅包含定义、声明而没有实际的可执行代码,那么这一行代码不会被统计到覆盖率中。目前已知不会统计的场景有:
- 
全局变量的定义,示例如下:
let HIGH_1_UInt8: UInt8 = 0b10000000; - 
成员变量仅声明未初始化赋值,示例如下:
public class StringBuilder <: Collection & ToString { private var myData: Array private var mySize: Int64 private var endIndex: Int64 } - 
仅有函数声明未包含函数体(包括
foreign函数等),示例如下:foreign func cj_core_free(p: CPointer): Unit - 
枚举类型定义,示例如下:
enum Numeric { NumDay | NumYearDay | NumYearWeek | NumHour12 | NumHour24 | NumMinute | NumSecond } - 
class、extend 等定义,其中 extend 和 class 所在的一行不会记录到覆盖率数据中,示例如下:
extend Int8 <: Formatter { // This line wil not account for the coverage. ... } public class StringBuilder <: Collection & ToString { // This line will not account for the coverage. ... } 
源代码中的 main 函数未被覆盖
原因: 使用 cjc --test 编译,仓颉测试框架会生成一个新的 main 作为程序入口,源代码中的 main 不再作为程序入口并且不会被执行。
建议: 在使用 cjc --test 之后,建议不用再手写多余的 main 。
FAQ
报错找不到 llvm-cov 命令
解决方法:
方法 1:设置 CANGJIE_HOME 环境变量, cjcov 可通过 CANGJIE_HOME 环境变量找到 llvm-cov 命令,设置方法如下:
假设 which cjc 显示 /work/cangjie/bin/cjc, 并且 /work/cangjie/bin/llvm/bin 和 /work/cangjie/bin/llvm/lib 目录都存在,则可设置:
export CANGJIE_HOME=/work/cangjie
方法 2:在 /root/.bashrc 里面直接设置环境变量,如 cjc 放在 /work/cangjie/bin/cjc 目录下,则设置方法如下:
export PATH=/work/cangjie/bin/llvm/bin:$PATH
export LIBRARY_PATH=/work/cangjie/bin/llvm/lib:$LIBRARY_PATH
export LD_LIBRARY_PATH=/work/cangjie/bin/llvm/lib:$LD_LIBRARY_PATH
方法 3:手动安装 llvm-cov 命令,如 ubuntu 上可执行命令:
apt install llvm-cov
出现 VirtualMachineError OutOfMemoryError
问题现象:
An exception has occurred:
Error VirtualMachineError OutOfMemoryError
解决方法: 仓颉默认规格 stack 1MB,heap 256 MB,建议根据文件数量大小将堆内存调到合适的大小。通常 2GB 的内存能够满足大多数情况,如果不够用则根据具体情况再增加内存大小。
示例:
把堆内存扩大到2GB:
export cjHeapSize=2GB