Prueba

Informar un problema Ver fuente Por la noche · 7.2 · 7.1 · 7.0 · 6.5 · 6.4

Existen varios enfoques diferentes para probar el código de Starlark en Bazel. Esta recopila las prácticas recomendadas y los frameworks actuales por caso de uso.

Cómo probar reglas

Skylib tiene un framework de prueba llamado unittest.bzl para comprobar el comportamiento de análisis-tiempo de las reglas, como sus acciones y proveedores. Estas pruebas se denominan "pruebas de análisis" y, actualmente, son las mejores para probar el funcionamiento interno de las reglas.

Algunas advertencias:

  • Las aserciones de prueba ocurren dentro de la compilación, no en un proceso del ejecutor de pruebas independiente. Los destinos que crea la prueba deben tener un nombre que no sea colisionen con objetivos de otras pruebas o de la compilación. Un error que que ocurre durante la prueba, Bazel la ve como una falla de compilación y no como una falla. y la prueba fallida.

  • Se requiere bastante código estándar para configurar las reglas que se están probando y las reglas que contienen las aserciones de prueba. Esta plantilla puede parecer abrumadora antes de empezar. Es útil tener en cuenta que las macros y los destinos generados durante la fase de carga, mientras que la regla de implementación no se ejecutan hasta más adelante, durante la fase de análisis.

  • Las pruebas de análisis están diseñadas para ser bastante pequeñas y livianas. Cierto del framework de pruebas de análisis se limitan a la verificación objetivos con una cantidad máxima de dependencias transitivas (actualmente, 500). Esto se debe a las implicaciones de rendimiento que tiene usar estas funciones con y pruebas.

El principio básico es definir una regla de prueba que depende del la regla que está a prueba. Esto le da a la regla de prueba acceso a los atributos proveedores.

La función de implementación de la regla de prueba realiza aserciones. Si hay fallas, estas no se generan de inmediato llamando a fail() (que un error de compilación en el momento del análisis), sino almacenarlos en un 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 de juguete, seguido de otro que comprueba 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 sentencias load() iniciales, existen dos partes principales en el archivo:

  • Las pruebas en sí, cada una de las cuales consiste en 1) un período de análisis de implementación para la regla de prueba, 2) una declaración del regla de prueba mediante analysistest.make(), y 3) una función de tiempo de carga (macro) para declarar la regla que está a prueba (y sus dependencias) y realizar pruebas . Si las aserciones no cambian entre los casos de prueba, los puntos 1) y 2) se pueden compartidos en varios casos de prueba.

  • La función del paquete de pruebas, que llama a las funciones de tiempo de carga de cada y declara un objetivo test_suite que agrupa todas las pruebas.

Para mantener la coherencia, sigue la convención de nombres recomendada: Significa que foo representa la parte del nombre de la prueba que describe lo que la prueba está verificando (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 a prueba debería ser llamado _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 para la regla de prueba debería tener el nombre _foo_test_impl (_provider_contents_test_impl)

  • las etiquetas de los destinos de las reglas que se están probando y sus dependencias debe 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 Build, por lo que es útil usar un nombre único para la prueba.

Pruebas de fallas

Puede ser útil verificar que una regla falla en función de ciertas entradas o en determinadas entradas para cada estado. Esto se puede hacer con el marco de trabajo de prueba 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 del error. (específicamente, el mensaje de error):

def _failure_testing_test_impl(ctx):
    env = analysistest.begin(ctx)
    asserts.expect_failure(env, "This rule should never work")
    return analysistest.end(env)

Además, asegúrate de que el destino que estás probando esté etiquetado específicamente como "manual". Sin esto, compilar todos los destinos en tu paquete con :all generará una del destino que falla intencionalmente y expondrá un error de compilación. Con "manual", el destino bajo prueba se compilará solo si se especifica de forma explícita, o bien una 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.

Verifica las acciones registradas

Se recomienda escribir pruebas que realicen aserciones sobre las acciones que tu registros de reglas, por ejemplo, mediante ctx.actions.run(). Puedes hacerlo en tu de implementación de reglas 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) muestra una lista de Action que representan acciones registradas por el el destino a prueba.

Verifica el comportamiento de la regla con diferentes marcas

Te recomendamos que verifiques que tu regla real se comporte de una manera determinada según se determina una compilación marcas. 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 objetivo de prueba con el marcas de compilación deseadas:

bazel test //mypkg:myrules_test -c opt

Pero, luego, resulta imposible que tu paquete de pruebas contenga simultáneamente un test que verifica el comportamiento de la regla en -c opt y otra prueba que verifica el comportamiento de la regla en -c dbg. No se podrán ejecutar las dos pruebas en la misma compilación.

Para solucionar este problema, debes especificar las marcas de compilación deseadas cuando definas la prueba. regla:

myrule_c_opt_test = analysistest.make(
    _myrule_c_opt_test_impl,
    config_settings = {
        "//command_line_option:compilation_mode": "opt",
    },
)

Por lo general, el destino que se está probando se analiza según las marcas de compilación actuales. Si especificas config_settings, se anulan los valores de la línea de comandos especificada. opciones de estado. (Las opciones no especificadas conservarán sus valores de la línea de comandos).

En el diccionario de config_settings especificado, las marcas de línea de comandos deben con el prefijo de un valor de marcador de posición especial //command_line_option:, como se muestra arriba.

Validar artefactos

Las principales formas de comprobar que los archivos generados sean correctos son las siguientes:

  • Puedes escribir una secuencia de comandos de prueba en shell, Python o algún otro lenguaje. crea un destino del tipo de regla *_test adecuado.

  • Puedes usar una regla especializada para el tipo de prueba que quieras realizar.

Cómo usar un destino de prueba

La forma más directa de validar un artefacto es escribir una secuencia de comandos y Agrega un destino *_test a tu archivo BUILD. Los artefactos específicos que quieres debería ser una dependencia de datos de este destino. Si tu lógica de validación es reutilizable para varias pruebas, debe ser una secuencia de comandos que tome la línea de comandos argumentos controlados por el atributo args del destino de prueba. Este es un ejemplo que valida que el resultado de myrule anterior sea "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"],
)

Usa una regla personalizada

Una alternativa más complicada es escribir la secuencia de comandos de shell como una plantilla que se crea una instancia con una regla nueva. Esto implica más indirección y Starlark pero lleva a archivos BUILD más limpios. Como beneficio colateral, cualquier argumento el procesamiento previo se puede realizar en Starlark en lugar de en la secuencia de comandos, que se un poco más de autodocumentación, ya que usa marcadores de posición simbólicos (para sustituciones) en vez de numéricas (para los 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 plantilla, podrías tener integró la plantilla en el archivo .bzl como una cadena y la expandió durante la fase de análisis con el método str.format o el formato %.

Prueba las utilidades de Starlark

Skylib unittest.bzl se puede usar para probar funciones de utilidad (es decir, funciones que son ni las implementaciones de macros ni reglas). En lugar de usar los unittest.bzl analysistest de la biblioteca unittest. Para estos paquetes de pruebas, la La función de conveniencia unittest.suite() se puede usar 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.