| """Utilities for testing bazel.""" | |
| # | |
| # Copyright 2015 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. | |
| ### First, trivial tests that either always pass, always fail, | |
| ### or sometimes pass depending on a trivial computation. | |
| def success_target(ctx, msg): | |
| """Return a success for an analysis test. | |
| The test rule must have an executable output. | |
| Args: | |
| ctx: the Bazel rule context | |
| msg: an informative message to display | |
| Returns: | |
| a suitable rule implementation struct(), | |
| with actions that always succeed at execution time. | |
| """ | |
| exe = ctx.outputs.executable | |
| dat = ctx.new_file(ctx.genfiles_dir, exe, ".dat") | |
| ctx.actions.write( | |
| output = dat, | |
| content = msg, | |
| ) | |
| ctx.actions.write( | |
| output = exe, | |
| content = "cat " + dat.path + " ; echo", | |
| is_executable = True, | |
| ) | |
| return struct(runfiles = ctx.runfiles([exe, dat])) | |
| def _successful_test_impl(ctx): | |
| return success_target(ctx, ctx.attr.msg) | |
| successful_test = rule( | |
| attrs = {"msg": attr.string(mandatory = True)}, | |
| executable = True, | |
| test = True, | |
| implementation = _successful_test_impl, | |
| ) | |
| def failure_target(ctx, msg): | |
| """Return a failure for an analysis test. | |
| The test rule must have an executable output. | |
| Args: | |
| ctx: the Bazel rule context | |
| msg: an informative message to display | |
| Returns: | |
| a suitable rule implementation struct(), | |
| with actions that always fail at execution time. | |
| """ | |
| ### fail(msg) ### <--- This would fail at analysis time. | |
| exe = ctx.outputs.executable | |
| dat = ctx.new_file(ctx.genfiles_dir, exe, ".dat") | |
| ctx.file_action( | |
| output = dat, | |
| content = msg, | |
| ) | |
| ctx.file_action( | |
| output = exe, | |
| content = "(cat " + dat.short_path + " ; echo ) >&2 ; exit 1", | |
| executable = True, | |
| ) | |
| return struct(runfiles = ctx.runfiles([exe, dat])) | |
| def _failed_test_impl(ctx): | |
| return failure_target(ctx, ctx.attr.msg) | |
| failed_test = rule( | |
| attrs = {"msg": attr.string(mandatory = True)}, | |
| executable = True, | |
| test = True, | |
| implementation = _failed_test_impl, | |
| ) | |
| ### Second, general purpose utilities | |
| def assert_(condition, string = "assertion failed", *args): | |
| """Trivial assertion mechanism. | |
| Args: | |
| condition: a generalized boolean expected to be true | |
| string: a format string for the error message should the assertion fail | |
| *args: format arguments for the error message should the assertion fail | |
| Returns: | |
| None. | |
| Raises: | |
| an error if the condition isn't true. | |
| """ | |
| if not condition: | |
| fail(string % args) | |
| def strip_prefix(prefix, string): | |
| assert_( | |
| string.startswith(prefix), | |
| "%s does not start with %s", | |
| string, | |
| prefix, | |
| ) | |
| return string[len(prefix):len(string)] | |
| def expectation_description(expect = None, expect_failure = None): | |
| """Turn expectation of result or error into a string.""" | |
| if expect_failure: | |
| return "failure " + str(expect_failure) | |
| else: | |
| return "result " + repr(expect) | |
| def check_results(result, failure, expect, expect_failure): | |
| """See if actual computation results match expectations. | |
| Args: | |
| result: the result returned by the test if it ran to completion | |
| failure: the failure message caught while testing, if any | |
| expect: the expected result for a successful test, if no failure expected | |
| expect_failure: the expected failure message for the test, if any | |
| Returns: | |
| a pair (tuple) of a boolean (true if success) and a message (string). | |
| """ | |
| wanted = expectation_description(expect, expect_failure) | |
| found = expectation_description(result, failure) | |
| if wanted == found: | |
| return (True, "successfully computed " + wanted) | |
| else: | |
| return (False, "expect " + wanted + " but found " + found) | |
| def load_results( | |
| name, | |
| result = None, | |
| failure = None, | |
| expect = None, | |
| expect_failure = None): | |
| """issue load-time results of a test. | |
| Args: | |
| name: the name of the Bazel rule at load time. | |
| result: the result returned by the test if it ran to completion | |
| failure: the failure message caught while testing, if any | |
| expect: the expected result for a successful test, if no failure expected | |
| expect_failure: the expected failure message for the test, if any | |
| Returns: | |
| None, after issuing a rule that will succeed at execution time if | |
| expectations were met. | |
| """ | |
| (is_success, msg) = check_results(result, failure, expect, expect_failure) | |
| this_test = successful_test if is_success else failed_test | |
| return this_test(name = name, msg = msg) | |
| def analysis_results( | |
| ctx, | |
| result = None, | |
| failure = None, | |
| expect = None, | |
| expect_failure = None): | |
| """issue analysis-time results of a test. | |
| Args: | |
| ctx: the Bazel rule context | |
| result: the result returned by the test if it ran to completion | |
| failure: the failure message caught while testing, if any | |
| expect: the expected result for a successful test, if no failure expected | |
| expect_failure: the expected failure message for the test, if any | |
| Returns: | |
| a suitable rule implementation struct(), | |
| with actions that succeed at execution time if expectation were met, | |
| or fail at execution time if they didn't. | |
| """ | |
| (is_success, msg) = check_results(result, failure, expect, expect_failure) | |
| this_test = success_target if is_success else failure_target | |
| return this_test(ctx, msg) | |
| ### Simple tests | |
| def _rule_test_impl(ctx): | |
| """check that a rule generates the desired outputs and providers.""" | |
| rule_ = ctx.attr.rule | |
| rule_name = str(rule_.label) | |
| exe = ctx.outputs.executable | |
| if ctx.attr.generates: | |
| # Generate the proper prefix to remove from generated files. | |
| prefix_parts = [] | |
| if rule_.label.workspace_root: | |
| # Create a prefix that is correctly relative to the output of this rule. | |
| prefix_parts = ["..", strip_prefix("external/", rule_.label.workspace_root)] | |
| if rule_.label.package: | |
| prefix_parts.append(rule_.label.package) | |
| prefix = "/".join(prefix_parts) | |
| if prefix: | |
| # If the prefix isn't empty, it needs a trailing slash. | |
| prefix = prefix + "/" | |
| # TODO(bazel-team): Use set() instead of sorted() once | |
| # set comparison is implemented. | |
| # TODO(bazel-team): Use a better way to determine if two paths refer to | |
| # the same file. | |
| generates = sorted(ctx.attr.generates) | |
| generated = sorted([ | |
| strip_prefix(prefix, f.short_path) | |
| for f in rule_.files.to_list() | |
| ]) | |
| if generates != generated: | |
| fail("rule %s generates %s not %s" % | |
| (rule_name, repr(generated), repr(generates))) | |
| provides = ctx.attr.provides | |
| if provides: | |
| files = [] | |
| commands = [] | |
| for k in provides.keys(): | |
| if hasattr(rule_, k): | |
| v = repr(getattr(rule_, k)) | |
| else: | |
| fail(("rule %s doesn't provide attribute %s. " + | |
| "Its list of attributes is: %s") % | |
| (rule_name, k, dir(rule_))) | |
| file_ = ctx.new_file(ctx.genfiles_dir, exe, "." + k) | |
| files += [file_] | |
| regexp = provides[k] | |
| commands += [ | |
| "if ! grep %s %s ; then echo 'bad %s:' ; cat %s ; echo ; exit 1 ; fi" % | |
| (repr(regexp), file_.short_path, k, file_.short_path), | |
| ] | |
| ctx.file_action(output = file_, content = v) | |
| script = "\n".join(commands + ["true"]) | |
| ctx.file_action(output = exe, content = script, executable = True) | |
| return struct(runfiles = ctx.runfiles([exe] + files)) | |
| else: | |
| return success_target(ctx, "success") | |
| rule_test = rule( | |
| attrs = { | |
| "rule": attr.label(mandatory = True), | |
| "generates": attr.string_list(), | |
| "provides": attr.string_dict(), | |
| }, | |
| executable = True, | |
| test = True, | |
| implementation = _rule_test_impl, | |
| ) | |
| def _file_test_impl(ctx): | |
| """check that a file has a given content.""" | |
| exe = ctx.outputs.executable | |
| file_ = ctx.file.file | |
| content = ctx.attr.content | |
| regexp = ctx.attr.regexp | |
| matches = ctx.attr.matches | |
| if bool(content) == bool(regexp): | |
| fail("Must specify one and only one of content or regexp") | |
| if content and matches != -1: | |
| fail("matches only makes sense with regexp") | |
| if content: | |
| dat = ctx.new_file(ctx.genfiles_dir, exe, ".dat") | |
| ctx.file_action( | |
| output = dat, | |
| content = content, | |
| ) | |
| ctx.file_action( | |
| output = exe, | |
| content = "diff -u %s %s" % (dat.short_path, file_.short_path), | |
| executable = True, | |
| ) | |
| return struct(runfiles = ctx.runfiles([exe, dat, file_])) | |
| if matches != -1: | |
| script = "[ %s == $(grep -c %s %s) ]" % ( | |
| matches, | |
| repr(regexp), | |
| file_.short_path, | |
| ) | |
| else: | |
| script = "grep %s %s" % (repr(regexp), file_.short_path) | |
| ctx.file_action( | |
| output = exe, | |
| content = script, | |
| executable = True, | |
| ) | |
| return struct(runfiles = ctx.runfiles([exe, file_])) | |
| file_test = rule( | |
| attrs = { | |
| "file": attr.label( | |
| mandatory = True, | |
| allow_files = True, | |
| single_file = True, | |
| ), | |
| "content": attr.string(default = ""), | |
| "regexp": attr.string(default = ""), | |
| "matches": attr.int(default = -1), | |
| }, | |
| executable = True, | |
| test = True, | |
| implementation = _file_test_impl, | |
| ) |