1# Copyright 2023 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"""# Util 16 17Various utilities to aid with testing. 18""" 19 20load("@bazel_skylib//lib:paths.bzl", "paths") 21load("@bazel_skylib//lib:types.bzl", "types") 22 23# TODO(ilist): remove references to skylib analysistest 24load("@bazel_skylib//lib:unittest.bzl", "analysistest") 25load("@bazel_skylib//rules:write_file.bzl", "write_file") 26 27_SKIP_CI_TAGS = [ 28 # copybara-marker: skip-ci-tag 29] 30 31# We add the manual tag to prevent implicitly building and running the subject 32# targets. When the rule-under-test is a test rule, it prevents trying to run 33# it. For binary rules, it prevents implicitly building it (and thus activating 34# more validation logic) when --build_tests_only is enabled. 35PREVENT_IMPLICIT_BUILDING_TAGS = [ 36 "manual", # Prevent `bazel ...` from directly building them 37 # copybara-marker: skip-coverage-tag 38] + _SKIP_CI_TAGS 39PREVENT_IMPLICIT_BUILDING = {"tags": PREVENT_IMPLICIT_BUILDING_TAGS} 40 41def merge_kwargs(*kwargs): 42 """Merges multiple dicts of kwargs. 43 44 This is similar to dict.update except: 45 * If a key's value is a list, it'll be concatenated to any existing value. 46 * An error is raised when the same non-list key occurs more than once. 47 48 Args: 49 *kwargs: kwarg arg dicts to merge 50 51 Returns: 52 dict of the merged kwarg dics. 53 """ 54 final = {} 55 for kwarg in kwargs: 56 for key, value in kwarg.items(): 57 if types.is_list(value): 58 final[key] = final.get(key, []) + value 59 elif key in final: 60 fail("Key already exists: {}: {}".format(key, final[key])) 61 else: 62 final[key] = value 63 return final 64 65def empty_file(name): 66 """Generates an empty file and returns the target name for it. 67 68 Args: 69 name: str, name of the generated output file. 70 71 Returns: 72 str, the name of the generated output. 73 """ 74 write_file( 75 name = "write_" + name, 76 content = [], 77 out = name, 78 ) 79 return name 80 81def helper_target(rule, **kwargs): 82 """Define a target only used as a Starlark test input. 83 84 This is useful for e.g. analysis tests, which have to setup a small 85 graph of targets that should only be built via the test (e.g. they 86 may require config settings the test sets). Tags are added to 87 hide the target from `:all`, `/...`, TAP, etc. 88 89 Args: 90 rule: rule-like function. 91 **kwargs: Any kwargs to pass to `rule`. Additional tags will 92 be added to hide the target. 93 """ 94 kwargs = merge_kwargs(kwargs, PREVENT_IMPLICIT_BUILDING) 95 rule(**kwargs) 96 97def short_paths(files_depset): 98 """Returns the `short_path` paths for a depset of files.""" 99 return [f.short_path for f in files_depset.to_list()] 100 101def runfiles_paths(workspace_name, runfiles): 102 """Returns the root-relative short paths for the files in runfiles. 103 104 Args: 105 workspace_name: str, the workspace name (`ctx.workspace_name`). 106 runfiles: runfiles, the runfiles to convert to short paths. 107 108 Returns: 109 list of short paths but runfiles root-relative. e.g. 110 'myworkspace/foo/bar.py'. 111 """ 112 paths = [] 113 paths.extend(short_paths(runfiles.files)) 114 paths.extend(runfiles.empty_filenames.to_list()) 115 paths.extend(_runfiles_symlink_paths(runfiles.symlinks)) 116 paths = _prepend_path(workspace_name, paths) 117 118 paths.extend(_runfiles_symlink_paths(runfiles.root_symlinks)) 119 return paths 120 121def runfiles_map(workspace_name, runfiles): 122 """Convert runfiles to a path->file mapping. 123 124 This approximates how Bazel materializes the runfiles on the file 125 system. 126 127 Args: 128 workspace_name: str; the workspace the runfiles belong to. 129 runfiles: runfiles; the runfiles to convert to a map. 130 131 Returns: 132 `dict[str, optional File]` that maps the path under the runfiles root 133 to it's backing file. The file may be None if the path came 134 from `runfiles.empty_filenames`. 135 """ 136 path_map = {} 137 workspace_prefix = workspace_name + "/" 138 for file in runfiles.files.to_list(): 139 path_map[workspace_prefix + file.short_path] = file 140 for path in runfiles.empty_filenames.to_list(): 141 path_map[workspace_prefix + path] = None 142 143 # NOTE: What happens when different files have the same symlink isn't 144 # exactly clear. For lack of a better option, we'll just take the last seen 145 # value. 146 for entry in runfiles.symlinks.to_list(): 147 path_map[workspace_prefix + entry.path] = entry.target_file 148 for entry in runfiles.root_symlinks.to_list(): 149 path_map[entry.path] = entry.target_file 150 return path_map 151 152def _prepend_path(prefix, path_strs): 153 return [paths.join(prefix, p) for p in path_strs] 154 155def _runfiles_symlink_paths(symlinks_depset): 156 return [entry.path for entry in symlinks_depset.to_list()] 157 158TestingAspectInfo = provider( 159 "Details about a target-under-test useful for testing.", 160 fields = { 161 "attrs": "The raw attributes of the target under test.", 162 "actions": "The actions registered for the target under test.", 163 "vars": "The var dict (ctx.var) for the target under text.", 164 "bin_path": "str; the ctx.bin_dir.path value (aka execroot).", 165 }, 166) 167 168def _testing_aspect_impl(target, ctx): 169 return [TestingAspectInfo( 170 attrs = ctx.rule.attr, 171 actions = target.actions, 172 vars = ctx.var, 173 bin_path = ctx.bin_dir.path, 174 )] 175 176# TODO(ilist): make private, after switching python tests to new testing framework 177testing_aspect = aspect( 178 implementation = _testing_aspect_impl, 179) 180 181# The same as `testing_aspect`, but recurses through all attributes in the 182# whole graph. This is useful if you need to extract information about 183# targets that aren't direct dependencies of the target under test, or to 184# reconstruct a more complete graph of inputs/outputs/generating-target. 185# TODO(ilist): make private, after switching python tests to new testing framework 186recursive_testing_aspect = aspect( 187 implementation = _testing_aspect_impl, 188 attr_aspects = ["*"], 189) 190 191def get_target_attrs(env): 192 return analysistest.target_under_test(env)[TestingAspectInfo].attrs 193 194# TODO(b/203567235): Remove this after cl/382467002 lands and the regular 195# `analysistest.target_actions()` can be used. 196def get_target_actions(env): 197 return analysistest.target_under_test(env)[TestingAspectInfo].actions 198 199def is_runfiles(obj): 200 """Tells if an object is a runfiles object.""" 201 return type(obj) == "runfiles" 202 203def is_file(obj): 204 """Tells if an object is a File object.""" 205 return type(obj) == "File" 206 207def skip_test(name): 208 """Defines a test target that is always skipped. 209 210 This is useful for tests that should be skipped if some condition, 211 determinable during the loading phase, isn't met. The resulting target will 212 show up as "SKIPPED" in the output. 213 214 If possible, prefer to use `target_compatible_with` to mark tests as 215 incompatible. This avoids confusing behavior where the type of a target 216 varies depending on loading-phase behavior. 217 218 Args: 219 name: The name of the target. 220 """ 221 _skip_test( 222 name = name, 223 target_compatible_with = ["@platforms//:incompatible"], 224 tags = _SKIP_CI_TAGS, 225 ) 226 227def _skip_test_impl(ctx): 228 _ = ctx # @unused 229 fail("Should have been skipped") 230 231_skip_test = rule( 232 implementation = _skip_test_impl, 233 test = True, 234) 235 236def _force_exec_config_impl(ctx): 237 return [DefaultInfo( 238 files = depset(ctx.files.tools), 239 default_runfiles = ctx.runfiles().merge_all([ 240 t[DefaultInfo].default_runfiles 241 for t in ctx.attr.tools 242 ]), 243 data_runfiles = ctx.runfiles().merge_all([ 244 t[DefaultInfo].data_runfiles 245 for t in ctx.attr.tools 246 ]), 247 )] 248 249force_exec_config = rule( 250 implementation = _force_exec_config_impl, 251 doc = "Rule to force arbitrary targets to `cfg=exec` so they can be " + 252 "tested when used as tools.", 253 attrs = { 254 "tools": attr.label_list( 255 cfg = "exec", 256 allow_files = True, 257 doc = "A list of tools to force into the exec config", 258 ), 259 }, 260) 261 262util = struct( 263 # keep sorted start 264 empty_file = empty_file, 265 force_exec_config = force_exec_config, 266 helper_target = helper_target, 267 merge_kwargs = merge_kwargs, 268 recursive_testing_aspect = recursive_testing_aspect, 269 runfiles_map = runfiles_map, 270 runfiles_paths = runfiles_paths, 271 short_paths = short_paths, 272 skip_test = skip_test, 273 testing_aspect = testing_aspect, 274 # keep sorted end 275) 276