Cobertura de código com o Bazel

O Bazel tem um subcomando coverage para produzir relatórios de cobertura de código em repositórios que podem ser testados com bazel coverage. Devido às peculiaridades dos vários ecossistemas de linguagem, nem sempre é trivial fazer isso funcionar para um determinado projeto.

Esta página documenta o processo geral para criar e visualizar relatórios de cobertura e também apresenta algumas observações específicas do idioma para linguagens cuja configuração é bem conhecida. É melhor ler primeiro a seção geral e depois os requisitos de um idioma específico. Consulte também a seção de execução remota, que exige algumas considerações adicionais.

Embora seja possível fazer muitas personalizações, este documento se concentra na produção e no consumo de lcov relatórios, que atualmente é o caminho mais bem aceito.

Como criar um relatório de cobertura

Preparação

O fluxo de trabalho básico para criar relatórios de cobertura exige o seguinte:

  • Um repositório básico com destinos de teste
  • Um conjunto de ferramentas com as ferramentas de cobertura de código específicas do idioma instaladas
  • Uma configuração de "instrumentação" correta

Os dois primeiros são específicos do idioma e, na maioria das vezes, simples, mas o último pode ser mais difícil para projetos complexos.

"Instrumentação", nesse caso, se refere às ferramentas de cobertura usadas para um destino específico. O Bazel permite ativar isso para um subconjunto específico de arquivos usando o --instrumentation_filter flag, que especifica um filtro para destinos testados com a instrumentação ativada. Para ativar a instrumentação para testes, o --instrument_test_targets flag é necessário.

Por padrão, o Bazel tenta corresponder aos pacotes de destino e imprime o filtro relevante como uma mensagem INFO.

Execução da cobertura

Para produzir um relatório de cobertura, use bazel coverage --combined_report=lcov [target]. Isso executa os testes do destino, gerando relatórios de cobertura no formato lcov para cada arquivo.

Quando terminar, o Bazel executa uma ação que coleta todos os arquivos de cobertura produzidos e os mescla em um, que é finalmente criado em $(bazel info output_path)/_coverage/_coverage_report.dat.

Os relatórios de cobertura também são produzidos se os testes falharem, mas isso não se estende aos testes com falha. Somente os testes aprovados são informados.

Visualização da cobertura

O relatório de cobertura é gerado apenas no formato lcov não legível. A partir dele, podemos usar o utilitário genhtml (parte do projeto lcov) para produzir um relatório que pode ser visualizado em um navegador da Web:

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

O genhtml também lê o código-fonte para anotar a cobertura ausente nesses arquivos. Para que isso funcione, espera-se que genhtml seja executado na raiz do projeto do Bazel.

Para conferir o resultado, basta abrir o arquivo index.html produzido no diretório genhtml em qualquer navegador da Web.

Para mais ajuda e informações sobre a ferramenta genhtml ou o formato de cobertura lcov consulte o projeto lcov.

Execução remota

A execução do teste com testes remotos tem algumas ressalvas:

  • A ação de combinação de relatórios ainda não pode ser executada remotamente. Isso ocorre porque o Bazel não considera os arquivos de saída de cobertura como parte do gráfico (consulte este problema) e, portanto, não pode tratá-los corretamente como entradas para a ação de combinação. Para contornar isso, use --strategy=CoverageReport=local.
    • Observação: pode ser necessário especificar algo como --strategy=CoverageReport=local,remote em vez disso, se o Bazel estiver configurado para tentar local,remote, devido à forma como ele resolve as estratégias.
  • --remote_download_minimal e flags semelhantes também não podem ser usados como consequência do anterior.
  • O Bazel falhará ao criar informações de cobertura se os testes tiverem sido armazenados em cache anteriormente. Para contornar isso, --nocache_test_results pode ser definido especificamente para execuções de cobertura, embora isso acarrete um custo alto em termos de tempos de teste.
  • --experimental_split_coverage_postprocessing e --experimental_fetch_all_coverage_outputs
    • Normalmente, a cobertura é executada como parte da ação de teste e, portanto, por padrão, não recebemos toda a cobertura como saídas da execução remota. Esses flags substituem o padrão e recebem os dados de cobertura. Consulte este problema para mais detalhes.

Configuração específica do idioma

Java

O Java funciona imediatamente com a configuração padrão. As cadeias de ferramentas do Bazel contêm tudo o que é necessário para a execução remota, incluindo o JUnit.

Python

Pré-requisitos

A execução da cobertura com o Python tem alguns pré-requisitos:

Como consumir o coverage.py modificado

Uma maneira de fazer isso é usando o rules_python, que oferece a capacidade de usar um arquivo requirements.txt. Os requisitos listados no arquivo são criados como destinos do Bazel usando a regra de repositório pip_install.

O requirements.txt precisa ter a seguinte entrada:

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

O arquivo rules_python, pip_install e requirements.txt precisa ser usado no arquivo WORKSPACE como:

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",
)

O requisito coverage.py pode ser consumido por destinos de teste definindo o seguinte nos arquivos BUILD:

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",
    ],
)

Se você estiver usando uma cadeia de ferramentas hermética do Python, em vez de adicionar a dependência de cobertura a cada destino py_test, adicione a ferramenta de cobertura à configuração da cadeia de ferramentas.

Como a regra pip_install depende da cadeia de ferramentas do Python, ela não pode ser usada para buscar o módulo coverage. Em vez disso, adicione ao seu WORKSPACE, por exemplo.

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",
    ],
)

Em seguida, configure sua cadeia de ferramentas do Python como, por exemplo.

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",
)