Laszlo Csomor | d784b5f | 2018-06-20 02:06:58 -0700 | [diff] [blame] | 1 | # Copyright 2018 The Bazel Authors. All rights reserved. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | """Defines a test rule that asserts the number of output files in another rule. |
| 16 | |
| 17 | This rule operates in Bazel's analysis phase, not in its execution phase, and so |
| 18 | it's faster than a conventional test rule would be. |
| 19 | |
| 20 | Furthermore this rule's action does not depend on any of the inputs (because the |
| 21 | assertion is done in the analysis phase) so Bazel won't even build the input |
| 22 | files to run the test. The test has constant execution time. |
| 23 | |
| 24 | === Use === |
| 25 | |
| 26 | Use this rule to assert the size of a filegroup or any other rule and catch |
| 27 | sudden, unexpected changes in the size. |
| 28 | |
| 29 | The `margin` attribute allows specifying a tolerance value (percentage), to |
| 30 | allow for organic, expected growth or shrinkage of the target rule. |
| 31 | |
| 32 | === Example === |
| 33 | |
| 34 | The "resources_size_test" test fails if the number of files in |
| 35 | "resources" changes from 123 by more than 3 percent: |
| 36 | |
| 37 | filegroup( |
| 38 | name = "resources", |
| 39 | srcs = glob(["**"]) + [ |
| 40 | "//foo/bar:resources" |
| 41 | "//baz:resources", |
| 42 | ], |
| 43 | ) |
| 44 | |
| 45 | rule_size_test( |
| 46 | name = "resources_size_test", |
| 47 | src = ":resources", |
| 48 | |
| 49 | # Expect 123 files in ":resources", with an error margin of 3% to allow |
| 50 | # for slight changes. |
| 51 | expect = 123, |
| 52 | margin = 3, |
| 53 | ) |
| 54 | """ |
| 55 | |
| 56 | def _impl(ctx): |
| 57 | if ctx.attr.expect < 0: |
| 58 | fail("ERROR: rule_size_test.expect must be positive") |
| 59 | |
| 60 | if ctx.attr.margin < 0 or ctx.attr.margin > 100: |
| 61 | # Do not allow more than 100% change in size. |
| 62 | fail("ERROR: rule_size_test.margin must be in range [0..100]") |
| 63 | |
| 64 | if ctx.attr.expect == 0 and ctx.attr.margin != 0: |
| 65 | # Allow no margin when expecting 0 files, to avoid division by zero. |
| 66 | fail("ERROR: rule_size_test.margin must be 0 when " + |
| 67 | "rule_size_test.expect is 0") |
| 68 | |
laurentlb | a48e10e | 2019-05-14 09:07:37 -0700 | [diff] [blame] | 69 | amount = len(ctx.attr.src[DefaultInfo].files.to_list()) |
Laszlo Csomor | d784b5f | 2018-06-20 02:06:58 -0700 | [diff] [blame] | 70 | |
| 71 | if ctx.attr.margin > 0: |
| 72 | if amount >= ctx.attr.expect: |
| 73 | diff = amount - ctx.attr.expect |
| 74 | else: |
| 75 | diff = ctx.attr.expect - amount |
| 76 | |
| 77 | if ((diff * 100) // ctx.attr.expect) > ctx.attr.margin: |
| 78 | fail(("ERROR: rule_size_test: expected %d file(s) within %d%% " + |
| 79 | "error margin, got %d file(s) (%d%% difference)") % ( |
| 80 | ctx.attr.expect, |
| 81 | ctx.attr.margin, |
| 82 | amount, |
| 83 | (diff * 100) // ctx.attr.expect, |
| 84 | )) |
| 85 | elif amount != ctx.attr.expect: |
| 86 | fail(("ERROR: rule_size_test: expected exactly %d file(s), got %d " + |
| 87 | "file(s)") % (ctx.attr.expect, amount)) |
| 88 | |
| 89 | if ctx.attr.is_windows: |
| 90 | test_bin = ctx.actions.declare_file(ctx.label.name + ".bat") |
Laszlo Csomor | baafedd | 2019-01-14 04:53:25 -0800 | [diff] [blame] | 91 | |
| 92 | # CreateProcessW can launch .bat files directly as long as they are NOT |
| 93 | # empty. Therefore we write a .bat file with a comment in it. |
| 94 | ctx.actions.write( |
| 95 | output = test_bin, |
| 96 | content = "@REM dummy", |
| 97 | is_executable = True, |
| 98 | ) |
Laszlo Csomor | d784b5f | 2018-06-20 02:06:58 -0700 | [diff] [blame] | 99 | else: |
| 100 | test_bin = ctx.actions.declare_file(ctx.label.name + ".sh") |
| 101 | ctx.actions.write( |
| 102 | output = test_bin, |
| 103 | content = "#!/bin/sh", |
| 104 | is_executable = True, |
| 105 | ) |
| 106 | |
| 107 | return [DefaultInfo(executable = test_bin)] |
| 108 | |
| 109 | _rule_size_test = rule( |
| 110 | implementation = _impl, |
| 111 | attrs = { |
| 112 | # The target whose number of output files this rule asserts. The number |
| 113 | # of output files is the size of the target's DefaultInfo.files field. |
| 114 | "src": attr.label(allow_files = True), |
| 115 | # A non-negative integer, the expected number of files that the target |
| 116 | # in `src` outputs. If 0, then `margin` must also be 0. |
| 117 | "expect": attr.int(mandatory = True), |
| 118 | # A percentage value, in the range of [0..100]. Allows for tolerance in |
| 119 | # the difference between expected and actual number of files in `src`. |
| 120 | # If 0, then the target in `src` must output exactly `expect` many |
| 121 | # files. |
| 122 | "margin": attr.int(mandatory = True), |
| 123 | # True if running on Windows, False otherwise. |
| 124 | "is_windows": attr.bool(mandatory = True), |
| 125 | }, |
| 126 | test = True, |
| 127 | ) |
| 128 | |
| 129 | def rule_size_test(name, **kwargs): |
| 130 | _rule_size_test( |
| 131 | name = name, |
| 132 | is_windows = select({ |
| 133 | "@bazel_tools//src/conditions:windows": True, |
| 134 | "//conditions:default": False, |
| 135 | }), |
| 136 | **kwargs |
| 137 | ) |