使用 Bazel 涵蓋程式碼

Bazel 提供 coverage 子指令,用於針對可透過 bazel coverage 測試的存放區產生程式碼涵蓋率報表。由於各種語言生態系統的慣用方法相輔相成,要針對特定專案執行這項工作並不容易。

本頁面記錄建立及查看涵蓋率報表的一般程序,並針對設定已知設定的語言提供部分特定語言的附註。建議您先詳閱一般章節,再瞭解特定語言的相關規定。另請注意遠端執行部分,且需要額外考量。

雖然已完成許多自訂作業,但本文件著重在產生及使用 lcov 報表,這是目前最受支援的路徑。

建立涵蓋率報表

準備

建立涵蓋率報表的基本工作流程如下:

  • 具有測試目標的基本存放區
  • 已安裝特定語言程式碼涵蓋率工具的工具鍊
  • 正確的「檢測」設定

前者對於語言特定且最簡單明瞭,但後者對於複雜的專案來說可能更加困難。

在此情況下,「Instrumentation」是指用於特定目標的涵蓋範圍工具。Bazel 可讓您使用 --instrumentation_filter 標記為特定檔案子集啟用這項功能,該標記會指定在已啟用檢測設備進行測試的目標篩選器。如要啟用測試的檢測功能,必須使用 --instrument_test_targets 標記。

根據預設,bazel 會嘗試比對目標套件,並以 INFO 訊息輸出相關篩選器。

累積涵蓋率

如要產生涵蓋率報表,請使用 bazel coverage --combined_report=lcov [target]。這項操作會對目標執行測試,並為每個檔案產生 lcov 格式的涵蓋範圍報表。

完成後,bazel 會執行一項動作來收集所有產生的涵蓋率檔案,並將其合併為一個檔案,最後在 $(bazel info output_path)/_coverage/_coverage_report.dat 下建立。

如果測試失敗,系統也會產生涵蓋率報表,但請注意,這並不涵蓋失敗的測試,系統只會回報通過的測試。

查看報導

涵蓋率報表只能以不人類可讀的 lcov 格式輸出。這時,我們可以使用 genhtml 公用程式 (屬於 lcov 專案的一部分) 產生可在網路瀏覽器中查看的報表:

genhtml --output genhtml "$(bazel info output_path)/_coverage/_coverage_report.dat"

請注意,genhtml 也會讀取原始碼,為這些檔案中缺少的涵蓋範圍加上註解。為了讓這項功能順利運作,genhtml 是在 bazel 專案的根目錄中執行。

如要查看結果,只要在任何網路瀏覽器中開啟 genhtml 目錄中產生的 index.html 檔案即可。

如需 genhtml 工具或 lcov 涵蓋範圍格式方面的進一步協助和資訊,請參閱 lcov 專案

遠端執行

以遠端測試執行作業執行目前需要注意的事項:

  • 報表組合動作目前無法從遠端執行。這是因為 Bazel 不會將涵蓋率輸出檔案視為圖表的一部分 (請參閱這個問題),因此無法正確將其視為組合動作的輸入內容。如要解決這個問題,請使用 --strategy=CoverageReport=local
    • 注意:由於 Bazel 如何解決策略,而將 Bazel 設為試用 local,remote,則可能需要改為指定 --strategy=CoverageReport=local,remote 之類的項目。
  • --remote_download_minimal 和類似的旗標也無法做為前者的結果使用。
  • 如果先前已快取測試,Bazel 目前無法建立涵蓋率資訊。如要解決這個問題,可以特別針對涵蓋範圍執行作業設定 --nocache_test_results,但這部分內容會在測試時間造成大量費用。
  • --experimental_split_coverage_postprocessing--experimental_fetch_all_coverage_outputs
    • 通常涵蓋範圍是測試動作的一部分執行,因此根據預設,我們並不會將所有涵蓋率傳回為遠端執行的輸出內容。這些標記會覆寫預設值,並取得涵蓋率資料。詳情請參閱這個問題

特定語言設定

Java

Java 應可在預設設定的情況下,以預設設定運作。bazel 工具鍊包含遠端執行所需的所有必要項目,包括 JUnit。

Python

必要條件

使用 Python 執行涵蓋範圍有一些先決條件:

使用修改後的涵蓋率.py

其中一個方法是透過 rules_python 來提供使用 requirements.txt 檔案的功能,接著檔案中列出的要求便會透過 pip_install 存放區規則建立為 bazel 目標。

requirements.txt 應包含下列項目:

git+https://github.com/ulfjack/coveragepy.git@lcov-support

接著,請在 WORKSPACE 檔案中使用 rules_pythonpip_installrequirements.txt 檔案,如下所示:

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "rules_python",
    url = "https://github.com/bazelbuild/rules_python/releases/download/0.5.0/rules_python-0.5.0.tar.gz",
    sha256 = "cd6730ed53a002c56ce4e2f396ba3b3be262fd7cb68339f0377a45e8227fe332",
)

load("@rules_python//python:pip.bzl", "pip_install")

pip_install(
   name = "python_deps",
   requirements = "//:requirements.txt",
)

然後,測試目標可以在 BUILD 檔案中設定下列內容,以使用涵蓋率.py 要求:

load("@python_deps//:requirements.bzl", "entry_point")

alias(
    name = "python_coverage_tools",
    actual = entry_point("coverage"),
)

py_test(
    name = "test",
    srcs = ["test.py"],
    env = {
        "PYTHON_COVERAGE": "$(location :python_coverage_tools)",
    },
    deps = [
        ":main",
        ":python_coverage_tools",
    ],
)

如果您使用的是密封 Python 工具鍊,可以改為在工具鍊設定中加入涵蓋率工具,而不是為每個 py_test 目標新增涵蓋率依附元件。

由於 pip_install 規則依附於 Python 工具鍊,因此無法用來擷取 coverage 模組。請改為在 WORKSPACE 中新增,例如

http_archive(
    name = "coverage_linux_x86_64"",
    build_file_content = """
py_library(
    name = "coverage",
    srcs = ["coverage/__main__.py"],
    data = glob(["coverage/*", "coverage/**/*.py"]),
    visibility = ["//visibility:public"],
)
""",
    sha256 = "84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3",
    type = "zip",
    urls = [
        "https://files.pythonhosted.org/packages/74/0d/0f3c522312fd27c32e1abe2fb5c323b583a5c108daf2c26d6e8dfdd5a105/coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
    ],
)

然後依範例設定 Python 工具鍊。

py_runtime(
    name = "py3_runtime_linux_x86_64",
    coverage_tool = "@coverage_linux_x86_64//:coverage",
    files = ["@python3_9_x86_64-unknown-linux-gnu//:files"],
    interpreter = "@python3_9_x86_64-unknown-linux-gnu//:bin/python3",
    python_version = "PY3",
)

py_runtime_pair(
    name = "python_runtimes_linux_x86_64",
    py2_runtime = None,
    py3_runtime = ":py3_runtime_linux_x86_64",
)

toolchain(
    name = "python_toolchain_linux_x86_64",
    exec_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
    toolchain = ":python_runtimes_linux_x86_64",
    toolchain_type = "@bazel_tools//tools/python:toolchain_type",
)