aboutsummaryrefslogtreecommitdiffhomepage
path: root/site/docs/skylark/testing.md
blob: f1a9225e6c76182d99577ef63af9138e73aadfad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
---
layout: documentation
title: Testing
---

# Testing

There are several different approaches to testing Skylark code in Bazel. This
page gathers the current best practices and frameworks by use case.

<!-- [TOC] -->


## For testing rules

[Skylib](https://github.com/bazelbuild/bazel-skylib) has a test framework called
[`unittest.bzl`](https://github.com/bazelbuild/bazel-skylib/blob/master/lib/unittest.bzl)
for checking the analysis-time behavior of rules, such as their actions and
providers. It is currently the best option for tests that need to access the
inner workings of Skylark rules.

Some caveats:

* Test assertions occur within the build, not a separate test runner process.
  Targets that are created by the test must be named such that they do not
  collide with targets from other tests or from the build. An error that occurs
  during the test is seen by Bazel as a build breakage rather than a test
  failure.

* It requires a fair amount of boilerplate to set up the rules under test and
  the rules containing test assertions. This boilerplate may seem daunting at
  first. It helps to [keep in mind](concepts.md#evaluation-model) which code
  runs during the loading phase and which code runs during the analysis phase.

* It cannot easily test for expected failures.

The basic principle is to define a testing rule that depends on the
rule-under-test. This gives the testing rule access to the rule-under-test’s
providers. There is experimental support for passing along action information
in the form of an additional provider.

The testing rule’s implementation function carries out assertions. If there are
any failures, these are not raised immediately by calling `fail()` (which would
trigger an analysis-time build error), but rather by storing the errors in a
generated script that fails at test execution time.

See below for a minimal toy example, followed by an example that checks actions.

### Minimal example

`//mypkg/BUILD`:

```python
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()
```

`//mypkg/myrules.bzl`:

```python
MyInfo = provider()

def _myrule_impl(ctx):
  """Rule that just generates a file and returns a provider."""
  ctx.actions.write(ctx.outputs.out, "abc")
  return [MyInfo(val="some value", out=ctx.outputs.out)]

myrule = rule(
    implementation = _myrule_impl,
    outputs = {"out": "%{name}.out"},
)
```

`//mypkg/myrules_test.bzl`:


```python
load("@bazel_skylib//:lib.bzl", "asserts", "unittest")
load(":myrules.bzl", "myrule", "MyInfo")

# ==== Check the provider contents ====

def _provider_contents_test_impl(ctx):
  # Analysis-time test logic; place assertions here. Always begins with begin()
  # and ends with end(). If you forget to call end(), you will get an error
  # about the test result file not having a generating action.
  env = unittest.begin(ctx)
  asserts.equals(env, "some value", ctx.attr.dep[MyInfo].val)
  # You can also use keyword arguments for readability if you prefer.
  asserts.equals(env,
    expected="some value",
    actual=ctx.attr.dep[MyInfo].val)
  unittest.end(env)

# Create the testing rule to wrap the test logic. Note that this must be bound
# to a global variable due to restrictions on how rules can be defined. Also,
# its name must end with "_test".
provider_contents_test = unittest.make(_provider_contents_test_impl,
                                       attrs={"dep": attr.label()})
# You can use a different attrs dict if you need to take in multiple rules for
# the same unit test, or if you need to test an aspect, or if you want to
# parameterize the assertions with different expected results.

# Macro to setup the test.
def test_provider_contents():
  # Rule under test.
  myrule(name = "provider_contents_subject")
  # Testing rule.
  provider_contents_test(name = "provider_contents",
                         dep = ":provider_contents_subject")


# 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():
  # Call all test functions and wrap their targets in a suite.
  test_provider_contents()
  # ...

  native.test_suite(
      name = "myrules_test",
      tests = [
          ":provider_contents",
          # ...
      ],
  )
````

The test can be run with `bazel test //mypkg:myrules_test`.

Aside from the initial `load()` statements, there are two main parts to the
file:

* The tests themselves, each of which consists of 1) an analysis-time
  implementation function for the testing rule, 2) a declaration of the testing
  rule via `unittest.make()`, and 3) a loading-time function (macro) for
  declaring the rule-under-test (and its dependencies) and testing rule. If the
  assertions do not change between test cases, 1) and 2) may be shared by
  multiple test cases.

* The test suite function, which calls the loading-time functions for each test,
  and declares a `test_suite` target bundling all tests together.

We recommend the following naming convention. Let `foo` stand for the part of
the test name that describes what the test is checking (`provider_contents` in
the above example). For example, a JUnit test method would be named `testFoo`.
Then:

* the loading-time function should should be named `test_foo`
  (`test_provider_contents`)

* its testing rule type should be named `foo_test` (`provider_contents_test`)

* the label of the target of this rule type should be `foo`
  (`provider_contents`)

* the implementation function for the testing rule should be named
  `_foo_test_impl` (`_provider_contents_test_impl`)

* the labels of the targets of the rules under test and their dependencies
  should be prefixed with `foo_` (`provider_contents_`)

Note that the labels of all targets can conflict with other labels in the same
BUILD package, so it’s helpful to use a unique name for the test.

### Actions example

To check that the `ctx.actions.write()` line works correctly, the above example
is modified as follows.

`//mypkg/myrules.bzl`:

```python
...

myrule = rule(
    implementation = _myrule_impl,
    outputs = {"out": "%{name}.out"},
    # This enables the Actions provider for this rule.
    _skylark_testable = True,
)
```

`//mypkg/myrules_test.bzl`:

```python
...

# ==== Check the emitted file_action ====

def _file_action_test_impl(ctx):
  env = unittest.begin(ctx)
  dep = ctx.attr.dep
  # Retrieve the Actions provider.
  actions = dep[Actions]
  # Retrieve the generating action for the output file.
  action = actions.by_file[dep.out]
  # Check the content that is to be written by the action.
  asserts.equals(env, action.content, "abc")
  unittest.end(env)

file_action_test = unittest.make(_file_action_test_impl,
                                 attrs={"dep": attr.label()})

def test_file_action():
  myrule(name = "file_action_subject")
  file_action_test(name = "file_action",
                   dep = ":file_action_subject")

...

def myrules_test_suite():
  # Call all test functions and wrap their targets in a suite.
  test_provider_contents()
  test_file_action()
  # ...

  native.test_suite(
      name = "myrules_test",
      tests = [
          ":provider_contents",
          ":file_action",
          # ...
      ]
),
```

The flag `"_skylark_testable = True"` is needed on any rule whose actions are to
be tested. This triggers the creation of the `Actions` provider. (The leading
underscore is because this API is still experimental.) The test logic for
actions makes use of the following API.

### Actions API

The [`Actions`](lib/globals.html#Actions) provider is retrieved like any other
(non-legacy) provider:

```python
ctx.attr.foo[Actions]
```

The returned object has a single field, `by_file`, which holds a dictionary
mapping each of the rule’s output files to its generating action. (Actions that
do not have output files, in particular those generated by
`ctx.actions.do_nothing()`, cannot be retrieved.)

The interface of the actions stored in the `by_file` map is documented
[here](lib/Action.html).

Finally, there is support for testing helper functions that are not rules, but
that take in a rule’s `ctx` in order to create actions on it. Use
`ctx.created_actions()` to get an `Actions` provider that has information about
all actions created on `ctx` up to the point that this function was called. For
this to work, the testing rule itself must have `"_skylark_testable=True"` set.
Testing rules created using `unittest.make()` automatically have this flag set.

## For validating artifacts

There are two main ways of checking that your generated files are correct: You
can write a test script in shell, Python, or another language, and create a
target of the appropriate `*_test` rule type; or you can use a specialized rule
for the kind of test you want to perform.

### Using a test target

The most straightforward way to validate an artifact is to write a script and
add a `*_test` target to your BUILD file. The specific artifacts you want to
check should be data dependencies of this target. If your validation logic is
reusable for multiple tests, it should be a script that takes command line
arguments that are controlled by the test target’s `args` attribute. Here’s an
example that validates that the output of `myrule` from above is `"abc"`.

`//mypkg/myrule_validator.sh`:

```bash
if [ "$(cat $1)" = "abc" ]; then
  echo "Passed"
  exit 0
else
  echo "Failed"
  exit 1
fi
```

`//mypkg/BUILD`:

```python
...

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

### Using a custom rule

A more complicated alternative is to write the shell script as a template that
gets instantiated by a new Skylark rule. This involves more indirection and
Skylark logic, but leads to cleaner BUILD files. As a side-benefit, any argument
preprocessing can be done in Skylark instead of the script, and the script is
slightly more self-documenting since it uses symbolic placeholders (for
substitutions) instead of numeric ones (for arguments).

`//mypkg/myrule_validator.sh.template`:

```bash
if [ "$(cat %TARGET%)" = "abc" ]; then
  echo "Passed"
  exit 0
else
  echo "Failed"
  exit 1
fi
```

`//mypkg/myrule_validation.bzl`:

```python
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(single_file=True),
           # We 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(single_file=True,
                                 default=Label("//mypkg:myrule_validator"))},
  test = True,
)
```

`//mypkg/BUILD`:

```python
...

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 we 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 we want to override it).
myrule_validation_test(
    name = "validate_mytarget",
    target = ":mytarget",
)
```

Alternatively, instead of using a template expansion action, we could have
inlined the template into the .bzl file as a string and expanded it during the
analysis phase using the `str.format` method or `%`-formatting.


## For testing Skylark utilities

The same framework that was used to test rules can also be used to test utility
functions (i.e., functions that are neither macros nor rule implementations).
There is no need to pass an `attrs` argument to `unittest.make()`, and there is
no special loading-time setup code to instantiate any rules-under-test. The
convenience function `unittest.suite()` can be used to reduce boilerplate in
this case.

`//mypkg/BUILD`:

```python
load(":myhelpers_test.bzl", "myhelpers_test_suite")

myhelpers_test_suite()
```

`//mypkg/myhelpers.bzl`:

```python
def myhelper():
    return "abc"
```

`//mypkg/myhelpers_test.bzl`:


```python
load("@bazel_skylib//:lib.bzl", "asserts", "unittest")
load(":myhelpers.bzl", "myhelper")

def _myhelper_test_impl(ctx):
  env = unittest.begin(ctx)
  asserts.equals(env, "abc", myhelper())
  unittest.end(env)

myhelper_test = unittest.make(_myhelper_test_impl)

# No need for a test_myhelper() setup function.

def myhelpers_test_suite():
  # unittest.suite() takes care of instantiating the testing rules and creating
  # a test_suite.
  unittest.suite(
    "myhelpers_tests",
    myhelper_test,
    # ...
  )
````

For more examples, see Skylib’s own [tests](https://github.com/bazelbuild/bazel-skylib/blob/master/tests/BUILD).

This can also be used when the utility function takes in a rule’s `ctx` object
as a parameter. If the behavior of the utility function requires that the rule
be defined in a certain way, you may have to pass in an `attrs` parameter to
`unittest.make()` after all, or you may have to declare the rule manually using
`rule()`. To test helpers that create actions, make the unit test rule set
`"_skylark_testable=True"` (if it is not created via `unittest.make()`) and
write assertions on the result of `ctx.created_actions()`, as described above.