| # Copyright 2018 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Defines a test rule that asserts the number of output files in another rule. |
| |
| This rule operates in Bazel's analysis phase, not in its execution phase, and so |
| it's faster than a conventional test rule would be. |
| |
| Furthermore this rule's action does not depend on any of the inputs (because the |
| assertion is done in the analysis phase) so Bazel won't even build the input |
| files to run the test. The test has constant execution time. |
| |
| === Use === |
| |
| Use this rule to assert the size of a filegroup or any other rule and catch |
| sudden, unexpected changes in the size. |
| |
| The `margin` attribute allows specifying a tolerance value (percentage), to |
| allow for organic, expected growth or shrinkage of the target rule. |
| |
| === Example === |
| |
| The "resources_size_test" test fails if the number of files in |
| "resources" changes from 123 by more than 3 percent: |
| |
| filegroup( |
| name = "resources", |
| srcs = glob(["**"]) + [ |
| "//foo/bar:resources" |
| "//baz:resources", |
| ], |
| ) |
| |
| rule_size_test( |
| name = "resources_size_test", |
| src = ":resources", |
| |
| # Expect 123 files in ":resources", with an error margin of 3% to allow |
| # for slight changes. |
| expect = 123, |
| margin = 3, |
| ) |
| """ |
| |
| def _impl(ctx): |
| if ctx.attr.expect < 0: |
| fail("ERROR: rule_size_test.expect must be positive") |
| |
| if ctx.attr.margin < 0 or ctx.attr.margin > 100: |
| # Do not allow more than 100% change in size. |
| fail("ERROR: rule_size_test.margin must be in range [0..100]") |
| |
| if ctx.attr.expect == 0 and ctx.attr.margin != 0: |
| # Allow no margin when expecting 0 files, to avoid division by zero. |
| fail("ERROR: rule_size_test.margin must be 0 when " + |
| "rule_size_test.expect is 0") |
| |
| amount = len(ctx.attr.src[DefaultInfo].files.to_list()) |
| |
| if ctx.attr.margin > 0: |
| if amount >= ctx.attr.expect: |
| diff = amount - ctx.attr.expect |
| else: |
| diff = ctx.attr.expect - amount |
| |
| if ((diff * 100) // ctx.attr.expect) > ctx.attr.margin: |
| fail(("ERROR: rule_size_test: expected %d file(s) within %d%% " + |
| "error margin, got %d file(s) (%d%% difference)") % ( |
| ctx.attr.expect, |
| ctx.attr.margin, |
| amount, |
| (diff * 100) // ctx.attr.expect, |
| )) |
| elif amount != ctx.attr.expect: |
| fail(("ERROR: rule_size_test: expected exactly %d file(s), got %d " + |
| "file(s)") % (ctx.attr.expect, amount)) |
| |
| if ctx.attr.is_windows: |
| test_bin = ctx.actions.declare_file(ctx.label.name + ".bat") |
| |
| # CreateProcessW can launch .bat files directly as long as they are NOT |
| # empty. Therefore we write a .bat file with a comment in it. |
| ctx.actions.write( |
| output = test_bin, |
| content = "@REM dummy", |
| is_executable = True, |
| ) |
| else: |
| test_bin = ctx.actions.declare_file(ctx.label.name + ".sh") |
| ctx.actions.write( |
| output = test_bin, |
| content = "#!/bin/sh", |
| is_executable = True, |
| ) |
| |
| return [DefaultInfo(executable = test_bin)] |
| |
| _rule_size_test = rule( |
| implementation = _impl, |
| attrs = { |
| # The target whose number of output files this rule asserts. The number |
| # of output files is the size of the target's DefaultInfo.files field. |
| "src": attr.label(allow_files = True), |
| # A non-negative integer, the expected number of files that the target |
| # in `src` outputs. If 0, then `margin` must also be 0. |
| "expect": attr.int(mandatory = True), |
| # A percentage value, in the range of [0..100]. Allows for tolerance in |
| # the difference between expected and actual number of files in `src`. |
| # If 0, then the target in `src` must output exactly `expect` many |
| # files. |
| "margin": attr.int(mandatory = True), |
| # True if running on Windows, False otherwise. |
| "is_windows": attr.bool(mandatory = True), |
| }, |
| test = True, |
| ) |
| |
| def rule_size_test(name, **kwargs): |
| _rule_size_test( |
| name = name, |
| is_windows = select({ |
| "@bazel_tools//src/conditions:windows": True, |
| "//conditions:default": False, |
| }), |
| **kwargs |
| ) |