| # 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) | 
 |  | 
 |     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 | 
 |     ) |