Cobertura de código com o Bazel

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

Nesta página, documentamos o processo geral de criação e visualização de relatórios de cobertura, além de algumas observações específicas para idiomas com configuração bem conhecida. Recomendamos ler primeiro a seção geral e, em seguida, os requisitos de uma linguagem específica. Observe também a seção de execução remota, que requer algumas considerações adicionais.

Embora seja possível muitas personalizações, este documento se concentra na produção e consumo de relatórios lcov, que atualmente é a rota mais aceita.

Como criar um relatório de cobertura

Preparação

O fluxo de trabalho básico para criar relatórios de cobertura requer 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 da linguagem instaladas
  • Uma configuração de "instrumentação" correta

As duas primeiras são específicas da linguagem e, na maioria das vezes, diretas. No entanto, a segunda pode ser mais difícil para projetos complexos.

Nesse caso, "instrumentação" se refere às ferramentas de cobertura usadas para um destino específico. O Bazel permite ativar esse recurso para um subconjunto específico de arquivos usando a flag --instrumentation_filter, que especifica um filtro para destinos testados com a instrumentação ativada. Para ativar a instrumentação para testes, a flag --instrument_test_targets é obrigatória.

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

Cobertura em execução

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

Depois de concluído, 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 serão gerados se os testes falharem. No entanto, isso não se estende aos testes com falha. Apenas 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 disso, podemos usar o utilitário genhtml, que faz 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"

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

Para ver 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 (link em inglês).

Execução remota

Atualmente, a execução de testes remotos tem algumas ressalvas:

  • Ainda não é possível executar remotamente a ação de combinação de relatórios. 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, se o Bazel estiver configurado para testar local,remote, devido à forma como ele resolve estratégias.
  • --remote_download_minimal e sinalizações semelhantes também não podem ser usadas como consequência da primeira.
  • Atualmente, o Bazel não vai criar informações de cobertura se os testes tiverem sido armazenados em cache anteriormente. Para contornar esse problema, --nocache_test_results pode ser definido especificamente para execuções de cobertura, mas, obviamente, isso gera um custo alto em termos de tempo de teste.
  • --experimental_split_coverage_postprocessing e --experimental_fetch_all_coverage_outputs
    • Normalmente, a cobertura é executada como parte da ação de teste. Portanto, por padrão, não recuperamos toda a cobertura como saídas da execução remota. Essas sinalizações substituem o padrão e coletam os dados de cobertura. Confira mais detalhes neste problema.

Configuração específica do idioma

Java

O Java deve funcionar imediatamente com a configuração padrão. Os conjuntos de ferramentas do bazel contêm tudo também para a execução remota, incluindo JUnit.

Python

Pré-requisitos

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

Como consumir ocover.py modificado

Uma maneira de fazer isso é por meio de rules_python, que possibilita usar um arquivo requirements.txt. Os requisitos listados nele são criados como destinos bazel usando a regra de repositório pip_install.

O requirements.txt precisa ter esta entrada:

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

Os arquivos rules_python, pip_install e requirements.txt precisam ser usados no arquivo do ESPAÇO DE TRABALHO 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 "cover.py" pode ser consumido por destinos de teste definindo o seguinte em 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 um conjunto de ferramentas Python hermético, em vez de adicionar a dependência de cobertura a cada destino py_test, adicione a ferramenta de cobertura à configuração do conjunto de ferramentas.

Como a regra pip_install depende do conjunto de ferramentas do Python, ela não pode ser usada para buscar o módulo coverage. Em vez disso, adicione 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 o conjunto de ferramentas do Python da seguinte forma:

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