Há várias abordagens diferentes para testar o código Starlark no Bazel. Isso reúne as práticas e estruturas recomendadas atuais por caso de uso.
Como testar regras
O Skylib (link em inglês) tem um framework de teste chamado
unittest.bzl
para verificar o comportamento das regras no tempo de análise, como as ações e
provedores de rede. Esses testes são chamados de "testes de análise" e atualmente são as melhores
opção para testar o funcionamento interno das regras.
Algumas ressalvas:
As declarações de teste ocorrem no build, não em um processo separado do executor de testes. Os destinos criados pelo teste precisam ser nomeados de modo a não colidir com destinos de outros testes ou do build. Um erro que ocorre durante o teste é vista pelo Bazel como uma falha de build, e não como falha no teste.
Requer uma quantidade razoável de código boilerplate para configurar as regras em teste e as regras que contêm declarações de teste. Esse padrão pode parecer assustador primeiro. É bom ter em mente que as macros são avaliados e as metas são geradas durante a fase de carregamento, as funções de implementação não são executadas até mais tarde, durante a fase de análise.
Os testes de análise devem ser relativamente pequenos e leves. Certo os recursos da estrutura de teste de análise são restritos a verificar destinos com um número máximo de dependências transitivas (atualmente 500). Isso se deve a implicações de desempenho do uso desses recursos com maiores provas.
O princípio básico é definir uma regra de teste que dependa da regra em teste. Isso dá à regra de teste acesso ao conjunto de dados provedores de rede.
A função de implementação da regra de teste realiza declarações. Se houver
caso haja falhas, elas não são geradas imediatamente chamando fail()
(que
acionar um erro de compilação no tempo de análise), mas armazenar os erros em um
um script gerado que falha no tempo de execução do teste.
Veja abaixo um exemplo de brinquedo mínimo, seguido por um exemplo que verifica as ações.
Exemplo mínimo
//mypkg/myrules.bzl
:
MyInfo = provider(fields = {
"val": "string value",
"out": "output File",
})
def _myrule_impl(ctx):
"""Rule that just generates a file and returns a provider."""
out = ctx.actions.declare_file(ctx.label.name + ".out")
ctx.actions.write(out, "abc")
return [MyInfo(val="some value", out=out)]
myrule = rule(
implementation = _myrule_impl,
)
//mypkg/myrules_test.bzl
:
load("@bazel_skylib//lib:unittest.bzl", "asserts", "analysistest")
load(":myrules.bzl", "myrule", "MyInfo")
# ==== Check the provider contents ====
def _provider_contents_test_impl(ctx):
env = analysistest.begin(ctx)
target_under_test = analysistest.target_under_test(env)
# If preferred, could pass these values as "expected" and "actual" keyword
# arguments.
asserts.equals(env, "some value", target_under_test[MyInfo].val)
# If you forget to return end(), you will get an error about an analysis
# test needing to return an instance of AnalysisTestResultInfo.
return analysistest.end(env)
# Create the testing rule to wrap the test logic. This must be bound to a global
# variable, not called in a macro's body, since macros get evaluated at loading
# time but the rule gets evaluated later, at analysis time. Since this is a test
# rule, its name must end with "_test".
provider_contents_test = analysistest.make(_provider_contents_test_impl)
# Macro to setup the test.
def _test_provider_contents():
# Rule under test. Be sure to tag 'manual', as this target should not be
# built using `:all` except as a dependency of the test.
myrule(name = "provider_contents_subject", tags = ["manual"])
# Testing rule.
provider_contents_test(name = "provider_contents_test",
target_under_test = ":provider_contents_subject")
# Note the target_under_test attribute is how the test rule depends on
# the real rule target.
# Entry point from the BUILD file; macro for running each test case's macro and
# declaring a test suite that wraps them together.
def myrules_test_suite(name):
# Call all test functions and wrap their targets in a suite.
_test_provider_contents()
# ...
native.test_suite(
name = name,
tests = [
":provider_contents_test",
# ...
],
)
//mypkg/BUILD
:
load(":myrules.bzl", "myrule")
load(":myrules_test.bzl", "myrules_test_suite")
# Production use of the rule.
myrule(
name = "mytarget",
)
# Call a macro that defines targets that perform the tests at analysis time,
# and that can be executed with "bazel test" to return the result.
myrules_test_suite(name = "myrules_test")
O teste pode ser executado com bazel test //mypkg:myrules_test
.
Além das instruções load()
iniciais, há duas partes principais na
arquivo:
Os testes em si, que consistem em 1) um tempo de análise função de implementação para a regra de teste, 2) uma declaração do regra de teste via
analysistest.make()
e 3) uma função de tempo de carregamento (macro) para declarar a regra em teste (e suas dependências) e testar regra de firewall. Se as declarações não mudarem entre os casos de teste, 1) e 2) poderão ser compartilhada por vários casos de teste.A função do pacote de testes, que chama as funções de tempo de carregamento para cada teste e declara um destino
test_suite
que agrupa todos os testes.
Para manter a consistência, siga a convenção de nomenclatura recomendada: deixe foo
representar
a parte do nome do teste que descreve o que o teste está verificando
(provider_contents
no exemplo acima). Por exemplo, um método de teste JUnit
se chamaria testFoo
.
Em seguida:
a macro que gera o teste e o alvo em teste deve ser chamado
_test_foo
(_test_provider_contents
)o tipo de regra de teste precisa ser chamado de
foo_test
(provider_contents_test
)o rótulo do destino desse tipo de regra precisa ser
foo_test
. (provider_contents_test
)a função de implementação da regra de teste precisa ser nomeada
_foo_test_impl
(_provider_contents_test_impl
)os rótulos dos destinos das regras em teste e as dependências deles precisa ter o prefixo
foo_
(provider_contents_
)
Os rótulos de todos os destinos podem entrar em conflito com outros rótulos no mesmo BUILD, então é útil usar um nome exclusivo para o teste.
Teste de falha
Pode ser útil verificar se uma regra falha com determinadas entradas ou em certos estado. Isso pode ser feito usando a estrutura de teste de análise:
A regra de teste criada com analysistest.make
precisa especificar expect_failure
:
failure_testing_test = analysistest.make(
_failure_testing_test_impl,
expect_failure = True,
)
A implementação da regra de teste precisa fazer declarações sobre a natureza da falha que ocorreu (especificamente, a mensagem de falha):
def _failure_testing_test_impl(ctx):
env = analysistest.begin(ctx)
asserts.expect_failure(env, "This rule should never work")
return analysistest.end(env)
Verifique também se a segmentação em teste está especificamente marcada como "manual".
Sem isso, criar todos os destinos no seu pacote usando :all
resultará em uma
do destino com falha intencional e causarão uma falha no build. Com
'manual', seu destino em teste será criado somente se especificado explicitamente ou conforme
uma dependência de um destino não manual (como sua regra de teste):
def _test_failure():
myrule(name = "this_should_fail", tags = ["manual"])
failure_testing_test(name = "failure_testing_test",
target_under_test = ":this_should_fail")
# Then call _test_failure() in the macro which generates the test suite and add
# ":failure_testing_test" to the suite's test targets.
Como verificar ações registradas
É recomendável criar testes que façam declarações sobre as ações
de regras são registradas, por exemplo, usando ctx.actions.run()
. Isso pode ser feito no
função de implementação da regra de teste de análise. Um exemplo:
def _inspect_actions_test_impl(ctx):
env = analysistest.begin(ctx)
target_under_test = analysistest.target_under_test(env)
actions = analysistest.target_actions(env)
asserts.equals(env, 1, len(actions))
action_output = actions[0].outputs.to_list()[0]
asserts.equals(
env, target_under_test.label.name + ".out", action_output.basename)
return analysistest.end(env)
Observe que analysistest.target_actions(env)
retorna uma lista de
Objetos Action
que representam ações registradas pelo
em teste.
Como verificar o comportamento da regra em sinalizações diferentes
Recomendamos que você verifique se a regra se comporta de uma determinada maneira em determinada versão de status. Por exemplo, sua regra poderá se comportar de maneira diferente se um usuário especificar:
bazel build //mypkg:real_target -c opt
versus
bazel build //mypkg:real_target -c dbg
À primeira vista, isso poderia ser feito testando o destino em teste usando o flags de build desejadas:
bazel test //mypkg:myrules_test -c opt
No entanto, se torna impossível para seu conjunto de testes conter simultaneamente uma
que verifica o comportamento da regra em -c opt
e outro teste que
verifica o comportamento da regra em -c dbg
. Não seria possível executar os dois testes
na mesma versão!
Isso pode ser resolvido especificando as flags de build desejadas ao definir o teste. regra:
myrule_c_opt_test = analysistest.make(
_myrule_c_opt_test_impl,
config_settings = {
"//command_line_option:compilation_mode": "opt",
},
)
Normalmente, um destino em teste é analisado de acordo com as flags de build atuais.
Especificar config_settings
substitui os valores da linha de comando especificada
. (As opções não especificadas manterão os valores do valor
linha de comando).
No dicionário config_settings
especificado, as sinalizações de linha de comando precisam ser
prefixado com um valor de marcador especial //command_line_option:
, como mostrado
acima.
Como validar artefatos
As principais maneiras de verificar se os arquivos gerados estão corretos são:
É possível criar um script de teste em shell, Python ou outra linguagem crie um destino do tipo de regra
*_test
apropriado.É possível usar uma regra especializada para o tipo de teste que você quer realizar.
Como usar um destino de teste
A maneira mais direta de validar um artefato é escrever um script e
adicione um destino *_test
ao arquivo BUILD. Os artefatos específicos que você quer
deve haver dependências de dados desse destino. Se a lógica de validação for
reutilizável para vários testes, ele precisa ser um script que usa a linha de comando
argumentos controlados pelo atributo args
do destino do teste. Este é um
exemplo que valida que a saída de myrule
acima é "abc"
.
//mypkg/myrule_validator.sh
:
if [ "$(cat $1)" = "abc" ]; then
echo "Passed"
exit 0
else
echo "Failed"
exit 1
fi
//mypkg/BUILD
:
...
myrule(
name = "mytarget",
)
...
# Needed for each target whose artifacts are to be checked.
sh_test(
name = "validate_mytarget",
srcs = [":myrule_validator.sh"],
args = ["$(location :mytarget.out)"],
data = [":mytarget.out"],
)
Usando uma regra personalizada
Uma alternativa mais complicada é escrever o script de shell como um modelo que seja instanciado por uma nova regra. Isso envolve mais indireção e Starlark mas gera arquivos BUILD mais limpos. Como benefício colateral, qualquer argumento o pré-processamento pode ser feito no Starlark ao invés do script, e o script é um pouco mais autodocumentado, já que usa espaços reservados simbólicos (para ) em vez de numéricas (para argumentos).
//mypkg/myrule_validator.sh.template
:
if [ "$(cat %TARGET%)" = "abc" ]; then
echo "Passed"
exit 0
else
echo "Failed"
exit 1
fi
//mypkg/myrule_validation.bzl
:
def _myrule_validation_test_impl(ctx):
"""Rule for instantiating myrule_validator.sh.template for a given target."""
exe = ctx.outputs.executable
target = ctx.file.target
ctx.actions.expand_template(output = exe,
template = ctx.file._script,
is_executable = True,
substitutions = {
"%TARGET%": target.short_path,
})
# This is needed to make sure the output file of myrule is visible to the
# resulting instantiated script.
return [DefaultInfo(runfiles=ctx.runfiles(files=[target]))]
myrule_validation_test = rule(
implementation = _myrule_validation_test_impl,
attrs = {"target": attr.label(allow_single_file=True),
# You need an implicit dependency in order to access the template.
# A target could potentially override this attribute to modify
# the test logic.
"_script": attr.label(allow_single_file=True,
default=Label("//mypkg:myrule_validator"))},
test = True,
)
//mypkg/BUILD
:
...
myrule(
name = "mytarget",
)
...
# Needed just once, to expose the template. Could have also used export_files(),
# and made the _script attribute set allow_files=True.
filegroup(
name = "myrule_validator",
srcs = [":myrule_validator.sh.template"],
)
# Needed for each target whose artifacts are to be checked. Notice that you no
# longer have to specify the output file name in a data attribute, or its
# $(location) expansion in an args attribute, or the label for the script
# (unless you want to override it).
myrule_validation_test(
name = "validate_mytarget",
target = ":mytarget",
)
Como alternativa, em vez de usar uma ação de expansão do modelo, é possível ter
embutiu o modelo no arquivo .bzl como uma string e o expandia durante a
fase de análise usando o método str.format
ou a formatação %
.
Como testar utilitários do Starlark
Skylibs
unittest.bzl
utilitário pode ser usado para testar funções utilitárias (ou seja, funções que são
nem macros nem implementações de regras). Em vez de usar unittest.bzl
Biblioteca analysistest
, unittest
pode ser usada. Para esses conjuntos de testes, os
função de conveniência unittest.suite()
pode ser usada para reduzir o código boilerplate.
//mypkg/myhelpers.bzl
:
def myhelper():
return "abc"
//mypkg/myhelpers_test.bzl
:
load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
load(":myhelpers.bzl", "myhelper")
def _myhelper_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(env, "abc", myhelper())
return unittest.end(env)
myhelper_test = unittest.make(_myhelper_test_impl)
# No need for a test_myhelper() setup function.
def myhelpers_test_suite(name):
# unittest.suite() takes care of instantiating the testing rules and creating
# a test_suite.
unittest.suite(
name,
myhelper_test,
# ...
)
//mypkg/BUILD
:
load(":myhelpers_test.bzl", "myhelpers_test_suite")
myhelpers_test_suite(name = "myhelpers_tests")
Para mais exemplos, consulte os testes do Skylib.