[译]使用Kover创建GitHub代码覆盖率徽章

JetBrains Kover Gradle插件是一个简单易用的JVM项目代码覆盖率解决方案。在大多数情况下,它甚至可以直接开箱即用。

只需应用该插件,就会有一个新的koverReport任务可用。运行时,它会生成一个可以打开和浏览的HTML覆盖率报告。

然而,虽然这很有用,但你可能很快就想把这个:

Kover示例报告

转换成可以添加到GitHub README中的徽章:

覆盖率徽章

本文将介绍如何实现这一目标。

整体概览

在开始之前,让我们先从宏观角度看看我们要做什么:

  • 在GitHub中创建一个gist
  • 生成一个认证令牌,使脚本可以覆写你的gist
  • 添加一个Gradle任务,输出你想要显示的覆盖率值
  • 创建一个脚本,运行该任务并将徽章值写入你的gist
  • 将从gist读取值的徽章添加到你的README中

本文不会详细介绍以下主题,如果我略过了你没有完全理解的内容,你可能需要参考它们的官方文档:

创建gist

通常,人们使用gist来相互分享代码片段,但从根本上说,gist只是GitHub为你托管的一个文本文件。

首先访问 https://gist.github.com/。

我们将创建一个虚拟的JSON文件。不用担心它的内容,因为它会在后面的步骤中被覆写。不过,GitHub不允许它为空,所以先随便输入一些文本开始。

你可以给文件取任何名字(如果你改变主意了,以后也很容易重命名)。真正的目标是获取GitHub为你的gist生成的唯一ID值。我建议使用<yourproject>-coverage-badge.json(将<yourproject>替换为你的实际项目名)。

GitHub创建gist

准备好后,点击Create Secret Gist按钮!

你会被带到一个新页面。查看该页面的URL以获取gist的ID:

GitHub gist ID

你暂时还不需要这个ID,但知道在哪里找到它是很好的。

创建gist令牌

我们希望允许脚本代表我们修改最近创建的gist。为此,我们需要创建一个可以用来授权编辑我们gist的令牌。

首先,登录GitHub并选择你的Settings页面:

GitHub顶级菜单

点击Developer settings菜单项,它在一个长列表的底部:

GitHub用户设置

进入后,点击Personal access tokens,然后点击Generate new token按钮:

GitHub用户开发者设置

我们将创建一个只能访问gist的令牌(限制潜在泄露造成的损害)。

为它创建任何你想要的描述。我选择了"Coverage badge gist editor",这样我以后就能记住为什么创建它。

接下来,我将我的令牌设置为永不过期。嘿,我只是一个开发开源爱好项目的普通人,所以我不太担心我的gist令牌被盗、在黑市上出售和滥用之类的事情。

然而,最佳实践要求我提到令牌应该过期,然后当它过期时,你应该重新创建一个新的并更新所有受影响的工作流。最终决定权在你。如果你犹豫不决,现在就创建一个不过期的令牌。你以后随时可以删除它。

只选择gist权限,然后点击Generate token按钮。

GitHub用户创建gist令牌

按下"复制"图标复制刚刚生成的令牌ID。

这个复制步骤非常重要,因为如果你在复制之前离开页面,ID就永远丢失了。 如果发生这种情况,你将不得不删除令牌并重做这一部分。

GitHub用户gist令牌已生成

Important

通常,在公开场合泄露这样的密钥是不好的!当然,在这种情况下,我早已删除了这个访问令牌,所以暴露它没有任何危害,我决定将它保留在这篇文章中作为教育目的。但请对你自己的令牌给予更多尊重!

创建gist密钥

现在我们已经将令牌ID复制到剪贴板中,我们想把它放在GitHub能够访问到的地方,而不是在某处以明文形式检入。这可以通过GitHub密钥来实现。

添加密钥很容易!访问你想要添加徽章的项目,打开它的Settings页面:

GitHub项目仪表板

点击Secrets > Actions,然后点击New repository secret按钮:

GitHub项目设置

为你的密钥选择一个名字。我们稍后会引用它,所以要记住它!我使用了GIST_SECRET

将令牌ID从剪贴板粘贴到Secret文本框中,然后按Add secret按钮:

GitHub项目创建密钥

现在就这样。让我们把注意力转向Gradle。

创建Gradle任务

在本文开始时,我提到koverReport生成HTML报告。这是对的,但它同时也生成XML报告。实际上,有koverHtmlReportkoverXmlReport任务可以直接运行。

Java标准库(Gradle暴露的)已经可以访问XML解析器,所以我们要做的是创建一个简单的任务,该任务依赖于koverXmlReport任务,加载它生成的XML文件,解析它,计算我们想要的覆盖率百分比,并将其打印到控制台。

Kover报告

Kover XML报告看起来像这样,我们感兴趣的覆盖率值存储在根report标签的子元素中:

<report name="Intellij Coverage Report">
  ...
  <counter type="INSTRUCTION" missed="6591" covered="5058"/>
  <counter type="BRANCH" missed="565" covered="236"/>
  <counter type="LINE" missed="809" covered="700"/>
  <counter type="METHOD" missed="375" covered="386"/>
  <counter type="CLASS" missed="194" covered="156"/>
</report>

在大多数情况下,当人们想到覆盖率时,他们可能想的是覆盖率。如果你想了解更多关于不同类型的覆盖率计数器的信息可以点击链接,但在本教程中,我们只会提取报告的"LINE"数据。

解析Kover报告的Gradle任务

在Gradle构建脚本(使用Kover插件的那个)中,在某处粘贴以下任务注册:

import javax.xml.parsers.DocumentBuilderFactory

// 重要!必须在plugins块中定义:
// plugins { id("org.jetbrains.kotlinx.kover") version ... }

tasks.register("printLineCoverage") {
    group = "verification" // 放入与`kover`任务相同的组
    dependsOn("koverXmlReport")
    doLast {
        val report = file("$buildDir/reports/kover/xml/report.xml")

        val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(report)
        val rootNode = doc.firstChild
        var childNode = rootNode.firstChild

        var coveragePercent = 0.0

        while (childNode != null) {
            if (childNode.nodeName == "counter") {
                val typeAttr = childNode.attributes.getNamedItem("type")
                if (typeAttr.textContent == "LINE") {
                    val missedAttr = childNode.attributes.getNamedItem("missed")
                    val coveredAttr = childNode.attributes.getNamedItem("covered")

                    val missed = missedAttr.textContent.toLong()
                    val covered = coveredAttr.textContent.toLong()

                    coveragePercent = (covered * 100.0) / (missed + covered)

                    break
                }
            }
            childNode = childNode.nextSibling
        }

        println("%.1f".format(coveragePercent))
    }
}

如果你想了解更多关于Java的DocumentBuilder类的信息,可以点击链接。但在上面的代码中,我们解析Kover生成的XML报告,循环遍历根("report")元素的所有子元素,直到找到一个名称为"counter"且具有"LINE"类型属性的元素。这段代码相当直观。

运行任务

要运行Gradle任务并隐藏其自身日志以便只显示你的输出,请在命令行参数中传入-q(或--quiet)。

换句话说,在终端中,你可以运行:

$ ./gradlew -q printLineCoverage
46.4

在进入下一步之前,请确认这对你是有效的。

GitHub Actions工作流

GitHub Actions是GitHub的自动化工作方法,通常用于持续集成。工作流是一个脚本,它定义了一个或多个相关的作业,这些作业会响应某个事件一起运行。

我们将创建一个工作流,每次有新代码推送到主分支时都会更新我们的徽章数据。

在你项目的.github/workflows文件夹中(如果不存在可以创建),创建一个YAML文件(我将我的命名为coverage-badge.yml):

# coverage-badge.yml

name: Create coverage badge

on:
  push:
    branches: [ main ] # !! CONFIRM THIS !!

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          java-version: '11' # !! CONFIRM THIS !!
          distribution: 'adopt'

      - name: Generate coverage output
        run: |
          echo "COVERAGE=$(${{github.workspace}}/gradlew -q printLineCoverage)" >> $GITHUB_ENV

      - name: Update dynamic badge gist
        uses: schneegans/[email protected]
        with:
          auth: ${{secrets.GIST_SECRET}} # !! CONFIRM THIS !!
          gistID: d6b5fcf2e961f94780a3dbbc11be023c # !! CHANGE THIS !!
          filename: myproject-coverage-badge.json  # !! CHANGE THIS !!
          label: coverage
          message: ${{env.COVERAGE}}%
          valColorRange: ${{env.COVERAGE}}
          minColorRange: 0
          maxColorRange: 100

Review the lines annotated above with !! CONFIRM THIS !! and !! CHANGE THIS !!.

In my project, the main branch is called main, but make sure that this is true for your project as well. Legacy projects may use master, for example.

After that, the first steps of the script tell GitHub to fetch the latest code and make sure Java 11 is available. You may need to use a higher JDK version in your own project, in case you're using any JDK 12+ features or standard library APIs.

Finally, be sure to update gistID and filename to your specific values.

You may copy the rest of the values as is.

步骤:生成覆盖率输出

这个工作流步骤运行我们的自定义Gradle任务(printLineCoverage),将其输出保存到一个变量(COVERAGE)中,该变量被放入一个可供脚本其余部分访问的环境中。

在工作流中设置环境变量通常是一个很方便的技巧。你可以在官方文档中了解更多相关信息。

Caution

如果你的项目中存在任何歧义(比如多个子模块使用Kover),你可能需要更明确地指定Gradle任务,例如:myproject:printLineCoverage

步骤:更新动态徽章gist

最后的工作流步骤配置Dynamic Badges操作,该操作将覆写我们之前创建的gist。

  • auth字段使用我们在前面章节保存的密钥。确保这里的名称与你之前选择的相匹配。
  • gistID字段应该设置为GitHub在创建gist时生成的ID(来自gist的URL)。
  • filename字段实际上可以设置为任何你想要的名称。如果你选择了一个与之前不同的名称,它会覆盖原来的。不过,最好还是设置为你之前使用的名称。
  • label字段是将显示在徽章左侧的文本。
  • message字段是将显示在徽章右侧的文本。注意,这里我们将其值设置为前一步骤Gradle任务的输出。我们在末尾添加一个"%",因为这样在展示给用户时看起来更好。

虽然你可以自己指定徽章的颜色,但Dynamic Badges操作支持一个便捷的功能:如果你声明一个范围以及该范围内的某个数值,它会自动为你设置颜色。

如果你的值在最小端,徽章将显示为红色,如果在最大端,则显示为绿色。中间的任何位置都会在渐变上进行插值,例如50%将显示为黄色。

为了利用这个功能,我们将minColorRange设置为0maxColorRange设置为100,并将valColorRange设置为前一步骤Gradle任务的输出。

Note

动态徽章还可以通过其他方式进行配置。详情请参阅官方文档

测试你的工作流

当你的工作流完成后,提交并推送它。转到你项目的Actions标签页,确保你看到工作流正在运行,并确认它最终成功。

GitHub项目操作

一旦你看到绿色的勾号,检查你的gist。它现在应该已经更新了真实的值!

GitHub gist已覆写

将徽章添加到你的README中

到这一步,我们就快完成了。要创建一个从JSON文件读取值的徽章,你可以使用shields.io端点API,使用以下代码片段:

![coverage badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/xxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy/raw/yourproject-coverage-badge.json)

其中,将xxxxxxx替换为你的用户名(例如bitspittle),*yyyyyyy...*替换为你的公共gistID(例如d6b5fcf2e961f94780a3dbbc11be023c),并将文件名替换为你的gist的最终文件名。

将该代码片段添加到你的README顶部,提交并推送到GitHub。

最后,访问你的项目,花点时间欣赏你的新徽章吧 —— 这确实需要不少工作!

实际应用中的覆盖率徽章

你可以在我的Kotter项目中看到我创建的覆盖率徽章(查看README顶部)。

Kotter标题

你可能想参考我的...

站在巨人的肩膀上

除了官方文档外,我发现以下资源特别有帮助:

结论

老实说,这个过程比我预期的要复杂得多。但是在项目的README页面上有一个覆盖率徽章确实很值得。

最后,你不必止步于此!通过组合Gradle任务、Dynamic Badges操作和GitHub Actions工作流,你绝对可以创建一些令人惊叹的自定义徽章。