1*d6050574SRomain Jobredeaux# Copyright 2022 The Bazel Authors. All rights reserved. 2*d6050574SRomain Jobredeaux# 3*d6050574SRomain Jobredeaux# Licensed under the Apache License, Version 2.0 (the "License"); 4*d6050574SRomain Jobredeaux# you may not use this file except in compliance with the License. 5*d6050574SRomain Jobredeaux# You may obtain a copy of the License at 6*d6050574SRomain Jobredeaux# 7*d6050574SRomain Jobredeaux# http://www.apache.org/licenses/LICENSE-2.0 8*d6050574SRomain Jobredeaux# 9*d6050574SRomain Jobredeaux# Unless required by applicable law or agreed to in writing, software 10*d6050574SRomain Jobredeaux# distributed under the License is distributed on an "AS IS" BASIS, 11*d6050574SRomain Jobredeaux# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*d6050574SRomain Jobredeaux# See the License for the specific language governing permissions and 13*d6050574SRomain Jobredeaux# limitations under the License. 14*d6050574SRomain Jobredeaux 15*d6050574SRomain Jobredeaux"""# Analysis test 16*d6050574SRomain Jobredeaux 17*d6050574SRomain JobredeauxSupport for testing analysis phase logic, such as rules. 18*d6050574SRomain Jobredeaux""" 19*d6050574SRomain Jobredeaux 20*d6050574SRomain Jobredeauxload("@bazel_skylib//lib:dicts.bzl", "dicts") 21*d6050574SRomain Jobredeauxload("//lib:truth.bzl", "truth") 22*d6050574SRomain Jobredeauxload("//lib:util.bzl", "recursive_testing_aspect", "testing_aspect") 23*d6050574SRomain Jobredeauxload("//lib/private:util.bzl", "get_test_name_from_function") 24*d6050574SRomain Jobredeaux 25*d6050574SRomain Jobredeauxdef _fail(env, msg): 26*d6050574SRomain Jobredeaux """Unconditionally causes the current test to fail. 27*d6050574SRomain Jobredeaux 28*d6050574SRomain Jobredeaux Args: 29*d6050574SRomain Jobredeaux env: The test environment returned by `unittest.begin`. 30*d6050574SRomain Jobredeaux msg: The message to log describing the failure. 31*d6050574SRomain Jobredeaux """ 32*d6050574SRomain Jobredeaux full_msg = "In test %s: %s" % (env.ctx.attr._impl_name, msg) 33*d6050574SRomain Jobredeaux 34*d6050574SRomain Jobredeaux # There isn't a better way to output the message in Starlark, so use print. 35*d6050574SRomain Jobredeaux # buildifier: disable=print 36*d6050574SRomain Jobredeaux print(full_msg) 37*d6050574SRomain Jobredeaux env.failures.append(full_msg) 38*d6050574SRomain Jobredeaux 39*d6050574SRomain Jobredeauxdef _begin_analysis_test(ctx): 40*d6050574SRomain Jobredeaux """Begins a unit test. 41*d6050574SRomain Jobredeaux 42*d6050574SRomain Jobredeaux This should be the first function called in a unit test implementation 43*d6050574SRomain Jobredeaux function. It initializes a "test environment" that is used to collect 44*d6050574SRomain Jobredeaux assertion failures so that they can be reported and logged at the end of the 45*d6050574SRomain Jobredeaux test. 46*d6050574SRomain Jobredeaux 47*d6050574SRomain Jobredeaux Args: 48*d6050574SRomain Jobredeaux ctx: The Starlark context. Pass the implementation function's `ctx` argument 49*d6050574SRomain Jobredeaux in verbatim. 50*d6050574SRomain Jobredeaux 51*d6050574SRomain Jobredeaux Returns: 52*d6050574SRomain Jobredeaux An analysis_test "environment" struct. The following fields are public: 53*d6050574SRomain Jobredeaux * ctx: the underlying rule ctx 54*d6050574SRomain Jobredeaux * expect: a truth Expect object (see truth.bzl). 55*d6050574SRomain Jobredeaux * fail: A function to register failures for later reporting. 56*d6050574SRomain Jobredeaux 57*d6050574SRomain Jobredeaux Other attributes are private, internal details and may change at any time. Do not rely 58*d6050574SRomain Jobredeaux on internal details. 59*d6050574SRomain Jobredeaux """ 60*d6050574SRomain Jobredeaux target = getattr(ctx.attr, "target") 61*d6050574SRomain Jobredeaux target = target[0] if type(target) == type([]) else target 62*d6050574SRomain Jobredeaux failures = [] 63*d6050574SRomain Jobredeaux failures_env = struct( 64*d6050574SRomain Jobredeaux ctx = ctx, 65*d6050574SRomain Jobredeaux failures = failures, 66*d6050574SRomain Jobredeaux ) 67*d6050574SRomain Jobredeaux truth_env = struct( 68*d6050574SRomain Jobredeaux ctx = ctx, 69*d6050574SRomain Jobredeaux fail = lambda msg: _fail(failures_env, msg), 70*d6050574SRomain Jobredeaux ) 71*d6050574SRomain Jobredeaux analysis_test_env = struct( 72*d6050574SRomain Jobredeaux ctx = ctx, 73*d6050574SRomain Jobredeaux # Visibility: package; only exposed so that our own tests can verify 74*d6050574SRomain Jobredeaux # failure behavior. 75*d6050574SRomain Jobredeaux _failures = failures, 76*d6050574SRomain Jobredeaux fail = truth_env.fail, 77*d6050574SRomain Jobredeaux expect = truth.expect(truth_env), 78*d6050574SRomain Jobredeaux ) 79*d6050574SRomain Jobredeaux return analysis_test_env, target 80*d6050574SRomain Jobredeaux 81*d6050574SRomain Jobredeauxdef _end_analysis_test(env): 82*d6050574SRomain Jobredeaux """Ends an analysis test and logs the results. 83*d6050574SRomain Jobredeaux 84*d6050574SRomain Jobredeaux This must be called and returned at the end of an analysis test implementation function so 85*d6050574SRomain Jobredeaux that the results are reported. 86*d6050574SRomain Jobredeaux 87*d6050574SRomain Jobredeaux Args: 88*d6050574SRomain Jobredeaux env: The test environment returned by `analysistest.begin`. 89*d6050574SRomain Jobredeaux 90*d6050574SRomain Jobredeaux Returns: 91*d6050574SRomain Jobredeaux A list of providers needed to automatically register the analysis test result. 92*d6050574SRomain Jobredeaux """ 93*d6050574SRomain Jobredeaux return [AnalysisTestResultInfo( 94*d6050574SRomain Jobredeaux success = (len(env._failures) == 0), 95*d6050574SRomain Jobredeaux message = "\n".join(env._failures), 96*d6050574SRomain Jobredeaux )] 97*d6050574SRomain Jobredeaux 98*d6050574SRomain Jobredeauxdef analysis_test( 99*d6050574SRomain Jobredeaux name, 100*d6050574SRomain Jobredeaux target, 101*d6050574SRomain Jobredeaux impl, 102*d6050574SRomain Jobredeaux expect_failure = False, 103*d6050574SRomain Jobredeaux attrs = {}, 104*d6050574SRomain Jobredeaux attr_values = {}, 105*d6050574SRomain Jobredeaux fragments = [], 106*d6050574SRomain Jobredeaux config_settings = {}, 107*d6050574SRomain Jobredeaux extra_target_under_test_aspects = [], 108*d6050574SRomain Jobredeaux collect_actions_recursively = False): 109*d6050574SRomain Jobredeaux """Creates an analysis test from its implementation function. 110*d6050574SRomain Jobredeaux 111*d6050574SRomain Jobredeaux An analysis test verifies the behavior of a "real" rule target by examining 112*d6050574SRomain Jobredeaux and asserting on the providers given by the real target. 113*d6050574SRomain Jobredeaux 114*d6050574SRomain Jobredeaux Each analysis test is defined in an implementation function. This function handles 115*d6050574SRomain Jobredeaux the boilerplate to create and return a test target and captures the 116*d6050574SRomain Jobredeaux implementation function's name so that it can be printed in test feedback. 117*d6050574SRomain Jobredeaux 118*d6050574SRomain Jobredeaux An example of an analysis test: 119*d6050574SRomain Jobredeaux 120*d6050574SRomain Jobredeaux ``` 121*d6050574SRomain Jobredeaux def basic_test(name): 122*d6050574SRomain Jobredeaux my_rule(name = name + "_subject", ...) 123*d6050574SRomain Jobredeaux 124*d6050574SRomain Jobredeaux analysistest(name = name, target = name + "_subject", impl = _your_test) 125*d6050574SRomain Jobredeaux 126*d6050574SRomain Jobredeaux def _your_test(env, target, actions): 127*d6050574SRomain Jobredeaux env.assert_that(target).runfiles().contains_at_least("foo.txt") 128*d6050574SRomain Jobredeaux env.assert_that(find_action(actions, generating="foo.txt")).argv().contains("--a") 129*d6050574SRomain Jobredeaux ``` 130*d6050574SRomain Jobredeaux 131*d6050574SRomain Jobredeaux Args: 132*d6050574SRomain Jobredeaux name: Name of the target. It should be a Starlark identifier, matching pattern 133*d6050574SRomain Jobredeaux '[A-Za-z_][A-Za-z0-9_]*'. 134*d6050574SRomain Jobredeaux target: The target to test. 135*d6050574SRomain Jobredeaux impl: The implementation function of the analysis test. 136*d6050574SRomain Jobredeaux expect_failure: If true, the analysis test will expect the target 137*d6050574SRomain Jobredeaux to fail. Assertions can be made on the underlying failure using truth.expect_failure 138*d6050574SRomain Jobredeaux attrs: An optional dictionary to supplement the attrs passed to the 139*d6050574SRomain Jobredeaux unit test's `rule()` constructor. 140*d6050574SRomain Jobredeaux attr_values: An optional dictionary of kwargs to pass onto the 141*d6050574SRomain Jobredeaux analysis test target itself (e.g. common attributes like `tags`, 142*d6050574SRomain Jobredeaux `target_compatible_with`, or attributes from `attrs`). Note that these 143*d6050574SRomain Jobredeaux are for the analysis test target itself, not the target under test. 144*d6050574SRomain Jobredeaux fragments: An optional list of fragment names that can be used to give rules access to 145*d6050574SRomain Jobredeaux language-specific parts of configuration. 146*d6050574SRomain Jobredeaux config_settings: A dictionary of configuration settings to change for the target under 147*d6050574SRomain Jobredeaux test and its dependencies. This may be used to essentially change 'build flags' for 148*d6050574SRomain Jobredeaux the target under test, and may thus be utilized to test multiple targets with different 149*d6050574SRomain Jobredeaux flags in a single build. NOTE: When values that are labels (e.g. for the 150*d6050574SRomain Jobredeaux --platforms flag), it's suggested to always explicitly call `Label()` 151*d6050574SRomain Jobredeaux on the value before passing it in. This ensures the label is resolved 152*d6050574SRomain Jobredeaux in your repository's context, not rule_testing's. 153*d6050574SRomain Jobredeaux extra_target_under_test_aspects: An optional list of aspects to apply to the target_under_test 154*d6050574SRomain Jobredeaux in addition to those set up by default for the test harness itself. 155*d6050574SRomain Jobredeaux collect_actions_recursively: If true, runs testing_aspect over all attributes, otherwise 156*d6050574SRomain Jobredeaux it is only applied to the target under test. 157*d6050574SRomain Jobredeaux 158*d6050574SRomain Jobredeaux Returns: 159*d6050574SRomain Jobredeaux (None) 160*d6050574SRomain Jobredeaux """ 161*d6050574SRomain Jobredeaux 162*d6050574SRomain Jobredeaux attrs = dict(attrs) 163*d6050574SRomain Jobredeaux attrs["_impl_name"] = attr.string(default = get_test_name_from_function(impl)) 164*d6050574SRomain Jobredeaux 165*d6050574SRomain Jobredeaux changed_settings = dict(config_settings) 166*d6050574SRomain Jobredeaux if expect_failure: 167*d6050574SRomain Jobredeaux changed_settings["//command_line_option:allow_analysis_failures"] = "True" 168*d6050574SRomain Jobredeaux 169*d6050574SRomain Jobredeaux target_attr_kwargs = {} 170*d6050574SRomain Jobredeaux if changed_settings: 171*d6050574SRomain Jobredeaux test_transition = analysis_test_transition( 172*d6050574SRomain Jobredeaux settings = changed_settings, 173*d6050574SRomain Jobredeaux ) 174*d6050574SRomain Jobredeaux target_attr_kwargs["cfg"] = test_transition 175*d6050574SRomain Jobredeaux 176*d6050574SRomain Jobredeaux attrs["target"] = attr.label( 177*d6050574SRomain Jobredeaux aspects = [recursive_testing_aspect if collect_actions_recursively else testing_aspect] + extra_target_under_test_aspects, 178*d6050574SRomain Jobredeaux mandatory = True, 179*d6050574SRomain Jobredeaux **target_attr_kwargs 180*d6050574SRomain Jobredeaux ) 181*d6050574SRomain Jobredeaux 182*d6050574SRomain Jobredeaux def wrapped_impl(ctx): 183*d6050574SRomain Jobredeaux env, target = _begin_analysis_test(ctx) 184*d6050574SRomain Jobredeaux impl(env, target) 185*d6050574SRomain Jobredeaux return _end_analysis_test(env) 186*d6050574SRomain Jobredeaux 187*d6050574SRomain Jobredeaux return testing.analysis_test( 188*d6050574SRomain Jobredeaux name, 189*d6050574SRomain Jobredeaux wrapped_impl, 190*d6050574SRomain Jobredeaux attrs = attrs, 191*d6050574SRomain Jobredeaux fragments = fragments, 192*d6050574SRomain Jobredeaux attr_values = dicts.add(attr_values, {"target": target}), 193*d6050574SRomain Jobredeaux ) 194