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