Existen varios enfoques diferentes para probar el código de Starlark en Bazel. En esta página, se recopilan las prácticas recomendadas y los frameworks actuales por caso de uso.
Cómo probar reglas
Skylib tiene un framework de pruebas llamado unittest.bzl
para verificar el comportamiento de las reglas en el momento del análisis, como sus acciones y proveedores. Estas pruebas se denominan "pruebas de análisis" y, actualmente, son la mejor opción para probar el funcionamiento interno de las reglas.
Algunas advertencias:
Las aserciones de prueba se producen dentro de la compilación, no en un proceso de ejecución de pruebas independiente. Los destinos que crea la prueba deben tener nombres que no entren en conflicto con los destinos de otras pruebas o de la compilación. Bazel considera que un error que se produce durante la prueba es una interrupción de la compilación en lugar de una falla de la prueba.
Se requiere una cantidad considerable de código estándar para configurar las reglas en prueba y las reglas que contienen aserciones de prueba. Este código boilerplate puede parecer abrumador al principio. Es importante tener en cuenta que las macros se evalúan y los destinos se generan durante la fase de carga, mientras que las funciones de implementación de reglas no se ejecutan hasta más adelante, durante la fase de análisis.
Las pruebas de análisis deben ser bastante pequeñas y ligeras. Ciertas funciones del framework de pruebas de análisis están restringidas a la verificación de destinos con una cantidad máxima de dependencias transitivas (actualmente, 500). Esto se debe a las implicaciones en el rendimiento que tiene el uso de estas funciones con pruebas más grandes.
El principio básico es definir una regla de prueba que dependa de la regla en prueba. Esto le da a la regla de prueba acceso a los proveedores de la regla en prueba.
La función de implementación de la regla de prueba lleva a cabo aserciones. Si hay fallas, no se generan de inmediato llamando a fail()
(lo que activaría un error de compilación en el momento del análisis), sino que se almacenan en una secuencia de comandos generada que falla en el momento de la ejecución de la prueba.
A continuación, se muestra un ejemplo mínimo y, luego, un ejemplo que verifica las acciones.
Ejemplo 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")
La prueba se puede ejecutar con bazel test //mypkg:myrules_test
.
Además de las instrucciones load()
iniciales, el archivo tiene dos partes principales:
Las pruebas en sí, cada una de las cuales consta de 1) una función de implementación en el momento del análisis para la regla de prueba, 2) una declaración de la regla de prueba a través de
analysistest.make()
y 3) una función (macro) en el momento de la carga para declarar la regla en prueba (y sus dependencias) y la regla de prueba. Si las aserciones no cambian entre los casos de prueba, 1) y 2) pueden compartirse entre varios casos de prueba.La función del conjunto de pruebas, que llama a las funciones de tiempo de carga para cada prueba y declara un destino
test_suite
que agrupa todas las pruebas.
Para mantener la coherencia, sigue la convención de nombres recomendada: Sea foo
la parte del nombre de la prueba que describe lo que esta verifica (provider_contents
en el ejemplo anterior). Por ejemplo, un método de prueba de JUnit se llamaría testFoo
.
Luego:
La macro que genera la prueba y el destino en prueba debe llamarse
_test_foo
(_test_provider_contents
).El tipo de regla de prueba debe llamarse
foo_test
(provider_contents_test
).La etiqueta del destino de este tipo de regla debe ser
foo_test
(provider_contents_test
).La función de implementación de la regla de prueba debe llamarse
_foo_test_impl
(_provider_contents_test_impl
).Las etiquetas de los destinos de las reglas en prueba y sus dependencias deben tener el prefijo
foo_
(provider_contents_
).
Ten en cuenta que las etiquetas de todos los destinos pueden entrar en conflicto con otras etiquetas del mismo paquete BUILD, por lo que es útil usar un nombre único para la prueba.
Pruebas de fallas
Puede ser útil verificar que una regla falla con ciertas entradas o en cierto estado. Esto se puede hacer con el framework de pruebas de análisis:
La regla de prueba creada con analysistest.make
debe especificar expect_failure
:
failure_testing_test = analysistest.make(
_failure_testing_test_impl,
expect_failure = True,
)
La implementación de la regla de prueba debe realizar aserciones sobre la naturaleza de la falla que se produjo (específicamente, el mensaje de falla):
def _failure_testing_test_impl(ctx):
env = analysistest.begin(ctx)
asserts.expect_failure(env, "This rule should never work")
return analysistest.end(env)
También asegúrate de que el objetivo que se está probando esté etiquetado específicamente como "manual".
Sin esto, compilar todos los destinos de tu paquete con :all
generará una compilación del destino que falla intencionalmente y mostrará un error de compilación. Con "manual", tu destino en prueba solo se compilará si se especifica de forma explícita o como dependencia de un destino no manual (como tu regla de prueba):
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.
Cómo verificar las acciones registradas
Es posible que desees escribir pruebas que realicen aserciones sobre las acciones que registra tu regla, por ejemplo, con ctx.actions.run()
. Esto se puede hacer en la función de implementación de la regla de prueba de análisis. Ejemplo:
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)
Ten en cuenta que analysistest.target_actions(env)
devuelve una lista de objetos Action
que representan las acciones registradas por el destino en prueba.
Cómo verificar el comportamiento de las reglas con diferentes marcas
Es posible que desees verificar que tu regla real se comporte de una manera determinada según ciertas marcas de compilación. Por ejemplo, tu regla puede comportarse de manera diferente si un usuario especifica lo siguiente:
bazel build //mypkg:real_target -c opt
versus
bazel build //mypkg:real_target -c dbg
A primera vista, esto se podría hacer probando el destino en prueba con las marcas de compilación deseadas:
bazel test //mypkg:myrules_test -c opt
Sin embargo, entonces se vuelve imposible que tu paquete de pruebas contenga simultáneamente una prueba que verifique el comportamiento de la regla en -c opt
y otra prueba que verifique el comportamiento de la regla en -c dbg
. Ambas pruebas no se podrían ejecutar en la misma compilación.
Para solucionar este problema, especifica las marcas de compilación deseadas cuando definas la regla de prueba:
myrule_c_opt_test = analysistest.make(
_myrule_c_opt_test_impl,
config_settings = {
"//command_line_option:compilation_mode": "opt",
},
)
Normalmente, se analiza un destino en prueba con las marcas de compilación actuales.
Si se especifica config_settings
, se anulan los valores de las opciones de línea de comandos especificadas. (Cualquier opción no especificada conservará sus valores de la línea de comandos real).
En el diccionario config_settings
especificado, las marcas de línea de comandos deben tener el prefijo de un valor de marcador de posición especial //command_line_option:
, como se muestra arriba.
Validación de artefactos
Estas son las principales formas de verificar que los archivos generados sean correctos:
Puedes escribir una secuencia de comandos de prueba en shell, Python o cualquier otro lenguaje, y crear un destino del tipo de regla
*_test
adecuado.Puedes usar una regla especializada para el tipo de prueba que deseas realizar.
Cómo usar un destino de prueba
La forma más sencilla de validar un artefacto es escribir un script y agregar un destino *_test
a tu archivo BUILD. Los artefactos específicos que deseas verificar deben ser dependencias de datos de este destino. Si tu lógica de validación se puede reutilizar para varias pruebas, debe ser una secuencia de comandos que tome argumentos de la línea de comandos controlados por el atributo args
del destino de la prueba. Este es un ejemplo que valida que el resultado de myrule
del ejemplo anterior es "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"],
)
Cómo usar una regla personalizada
Una alternativa más complicada es escribir el script de shell como una plantilla que se instancia con una regla nueva. Esto implica más indirección y lógica de Starlark, pero genera archivos BUILD más limpios. Como beneficio adicional, cualquier preprocesamiento de argumentos se puede realizar en Starlark en lugar de en la secuencia de comandos, y la secuencia de comandos se documenta un poco más por sí misma, ya que usa marcadores de posición simbólicos (para sustituciones) en lugar de numéricos (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, en lugar de usar una acción de expansión de plantillas, podrías haber insertado la plantilla en el archivo .bzl como una cadena y expandirla durante la fase de análisis con el método str.format
o el formato %
.
Prueba de utilidades de Starlark
El framework unittest.bzl
de Skylib se puede usar para probar funciones de utilidad (es decir, funciones que no son macros ni implementaciones de reglas). En lugar de usar la biblioteca analysistest
de unittest.bzl
, se puede usar unittest
. Para estos conjuntos de pruebas, se puede usar la función de conveniencia unittest.suite()
para reducir el código estándar.
//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 obtener más ejemplos, consulta las pruebas de Skylib.