收集WASM合约代码覆盖率

代码覆盖率信息是软件开发的一个重要技术指标。从0.10.2.14版本开始,MYCDT支持收集C++ WASM合约的代码覆盖率信息,您只需要在编译合约时增加参数--coverage来指示编译器进行代码插桩即可,然后正常部署运行合约,代码覆盖率信息会通过交易的Log返回,对应的topiccoverage,信息存放于data字段。

注意:此种模式下编译出来的合约字节码文件,不能用于生成环境。

接下来,我们仍旧以hello world合约为例来演示如何收集合约代码覆盖率。

编译合约

合约源代码如下。

#include <mychainlib/contract.h>

class Hello : public mychain::Contract {
public:
    INTERFACE void hi() {
        mychain::print("Hi");
    }
};

INTERFACE_EXPORT(Hello, (hi))

编译时使用参数--coverage

$ my++ hello.cc -o hello.wasm --coverage

编译完成后,除了合约字节码 hello.wasm 外,在当前目录下还会产生一个名为 hello.gcno 的文件。这个文件在生成覆盖率报告时将会用到,请不要删除。

运行合约

合约在链上执行时,会把动态生成的覆盖率信息保存到log中,topiccoverage,因此您需要在SDK侧获取topiccoveragelog信息,每条log的内容通过LEB128解码可以得到两个字段:覆盖率文件路径和覆盖率信息,请根据文件路径信息来保存覆盖率信息。覆盖率信息文件以.gcda结尾,对应于上面一步MYCDT生成的.gcno文件。

不同SDK收集覆盖率的具体实现不同,以C++SDK为例。

bool CallHi(const Identity& from) {
    // 调用合约
    auto params = std::make_shared<WASMParameter>();
    params->SetFunctionSelector("hi");
    auto req = std::make_shared<CallContractRequest>(from, contract_id, VMType::WASM, params, 0);
    auto res = client_ptr->GetContractService()->CallContract(req);
    if (res->GetReturnCode() != ErrorCode::SUCCESS || res->tx_receipt_.result_ != 0) {
        LOG_ERROR(env->logger_, "call contract failed, error code:%s, receipt result:%s",
                  StringForErrorCode(res->GetReturnCode()).c_str(),
                  StringForErrorCode(res->tx_receipt_.result_).c_str());
        return false;
    }

    // 保存覆盖率信息
    std::string topic = "636f766572616765"; // hex from "coverage"
    for (auto& log : res->tx_receipt_.logs_) {
        if (!log.MatchTopic(topic))
            continue;

        WASMOutput output;
        output.SetOutput(log.log_data_);
        auto path = output.GetString();
        auto data = output.GetString();

        std::ofstream fs(path, std::ios::binary);
        if (!fs.is_open())
            LOG_WARN(env->logger_, "failed to save file: %s", path.c_str());
        fs.write(data.data(), data.size());
    }
    return true;
}

生成覆盖率报告

使用MYCDT提供的llvm-gcov工具处理编译时生成的hello.gcno文件和运行时得到的hello.gcda文件,得到直观的覆盖率报告。

$ llvm-gcov hello.gcda

上述命令会隐式的寻找hello.gcno文件。最终会在当前目录生成一个名为hello.cc.gcov文件,用文本编辑器打开该文件可以看到代码覆盖率信息,如下所示。

        -:    0:Source:hello.cc
        -:    0:Graph:hello.gcno
        -:    0:Data:hello.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:#include <mychainlib/contract.h>
        -:    2:
        1:    3:class Hello : public mychain::Contract {
        -:    4:public:
        1:    5:    INTERFACE void hi() {
        1:    6:        mychain::print("Hi");
        1:    7:    }
        -:    8:};
        -:    9:
        2:   10:INTERFACE_EXPORT(Hello, (hi))

生成网页版覆盖率报告

如果要生成网页形式的覆盖率报告,需要使用1.13版本以上的第三方工具lcov,您可以根据以下安装方式完成此工具的安装,MYCDT中暂不单独提供。

  • 在Mac OS上安装lcov方式如下:

    先从https://brew.sh/ 安装homebrew工具,然后终端控制台下执行brew install lcov进行安装。

  • 在 CentOS 8上安装lcov方式如下(安装1.14版本的lcov):

    yum install -y lcov 
  • 在 Ubuntu 20.04上安装lcov方式如下(安装1.14版本的lcov):

    apt install -y lcov 

安装完成lcov后可以执行lcov -v检查安装的lcov版本大于1.13版本。

报告生成命令如下:

$ lcov -c -d . -o coverage.info --gcov-tool llvm-gcov
$ genhtml coverage.info -o coverage-html

上述命令执行完成后会生成一个coverage-html目录,用浏览器打开该目录下的index.html文件即可看到网页版的覆盖率报告。