blob: e328b715c53d682bf6ed91258d96be786556ba94 [file] [log] [blame]
# Copyright 2023 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.
"""# Util
Various utilities to aid with testing.
"""
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_skylib//lib:types.bzl", "types")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("//lib:unittest.bzl", "analysistest")
_SKIP_CI_TAGS = [
# copybara-marker: skip-ci-tag
]
# We add the manual tag to prevent implicitly building and running the subject
# targets. When the rule-under-test is a test rule, it prevents trying to run
# it. For binary rules, it prevents implicitly building it (and thus activating
# more validation logic) when --build_tests_only is enabled.
PREVENT_IMPLICIT_BUILDING_TAGS = [
"manual", # Prevent `bazel ...` from directly building them
# copybara-marker: skip-coverage-tag
] + _SKIP_CI_TAGS
PREVENT_IMPLICIT_BUILDING = {"tags": PREVENT_IMPLICIT_BUILDING_TAGS}
def merge_kwargs(*kwargs):
"""Merges multiple dicts of kwargs.
This is similar to dict.update except:
* If a key's value is a list, it'll be concatenated to any existing value.
* An error is raised when the same non-list key occurs more than once.
Args:
*kwargs: kwarg arg dicts to merge
Returns:
dict of the merged kwarg dics.
"""
final = {}
for kwarg in kwargs:
for key, value in kwarg.items():
if types.is_list(value):
final[key] = final.get(key, []) + value
elif key in final:
fail("Key already exists: {}: {}".format(key, final[key]))
else:
final[key] = value
return final
def empty_file(name):
"""Generates an empty file and returns the target name for it.
Args:
name: str, name of the generated output file.
Returns:
str, the name of the generated output.
"""
write_file(
name = "write_" + name,
content = [],
out = name,
)
return name
def helper_target(rule, **kwargs):
"""Define a target only used as a Starlark test input.
This is useful for e.g. analysis tests, which have to setup a small
graph of targets that should only be built via the test (e.g. they
may require config settings the test sets). Tags are added to
hide the target from `:all`, `/...`, TAP, etc.
Args:
rule: rule-like function.
**kwargs: Any kwargs to pass to `rule`. Additional tags will
be added to hide the target.
"""
kwargs = merge_kwargs(kwargs, PREVENT_IMPLICIT_BUILDING)
rule(**kwargs)
def short_paths(files_depset):
"""Returns the `short_path` paths for a depset of files."""
return [f.short_path for f in files_depset.to_list()]
def runfiles_paths(workspace_name, runfiles):
"""Returns the root-relative short paths for the files in runfiles.
Args:
workspace_name: str, the workspace name (`ctx.workspace_name`).
runfiles: runfiles, the runfiles to convert to short paths.
Returns:
list of short paths but runfiles root-relative. e.g.
'myworkspace/foo/bar.py'.
"""
paths = []
paths.extend(short_paths(runfiles.files))
paths.extend(runfiles.empty_filenames.to_list())
paths.extend(_runfiles_symlink_paths(runfiles.symlinks))
paths = _prepend_path(workspace_name, paths)
paths.extend(_runfiles_symlink_paths(runfiles.root_symlinks))
return paths
def runfiles_map(workspace_name, runfiles):
"""Convert runfiles to a path->file mapping.
This approximates how Bazel materializes the runfiles on the file
system.
Args:
workspace_name: str; the workspace the runfiles belong to.
runfiles: runfiles; the runfiles to convert to a map.
Returns:
`dict[str, optional File]` that maps the path under the runfiles root
to it's backing file. The file may be None if the path came
from `runfiles.empty_filenames`.
"""
path_map = {}
workspace_prefix = workspace_name + "/"
for file in runfiles.files.to_list():
path_map[workspace_prefix + file.short_path] = file
for path in runfiles.empty_filenames.to_list():
path_map[workspace_prefix + path] = None
# NOTE: What happens when different files have the same symlink isn't
# exactly clear. For lack of a better option, we'll just take the last seen
# value.
for entry in runfiles.symlinks.to_list():
path_map[workspace_prefix + entry.path] = entry.target_file
for entry in runfiles.root_symlinks.to_list():
path_map[entry.path] = entry.target_file
return path_map
def _prepend_path(prefix, path_strs):
return [paths.join(prefix, p) for p in path_strs]
def _runfiles_symlink_paths(symlinks_depset):
return [entry.path for entry in symlinks_depset.to_list()]
TestingAspectInfo = provider(
"Details about a target-under-test useful for testing.",
fields = {
"attrs": "The raw attributes of the target under test.",
"actions": "The actions registered for the target under test.",
"vars": "The var dict (ctx.var) for the target under text.",
"bin_path": "str; the ctx.bin_dir.path value (aka execroot).",
},
)
def _testing_aspect_impl(target, ctx):
return [TestingAspectInfo(
attrs = ctx.rule.attr,
actions = target.actions,
vars = ctx.var,
bin_path = ctx.bin_dir.path,
)]
# TODO(ilist): make private, after switching python tests to new testing framework
testing_aspect = aspect(
implementation = _testing_aspect_impl,
)
# The same as `testing_aspect`, but recurses through all attributes in the
# whole graph. This is useful if you need to extract information about
# targets that aren't direct dependencies of the target under test, or to
# reconstruct a more complete graph of inputs/outputs/generating-target.
# TODO(ilist): make private, after switching python tests to new testing framework
recursive_testing_aspect = aspect(
implementation = _testing_aspect_impl,
attr_aspects = ["*"],
)
def get_target_attrs(env):
return analysistest.target_under_test(env)[TestingAspectInfo].attrs
# TODO(b/203567235): Remove this after cl/382467002 lands and the regular
# `analysistest.target_actions()` can be used.
def get_target_actions(env):
return analysistest.target_under_test(env)[TestingAspectInfo].actions
def is_runfiles(obj):
"""Tells if an object is a runfiles object."""
return type(obj) == "runfiles"
def is_file(obj):
"""Tells if an object is a File object."""
return type(obj) == "File"
def skip_test(name):
"""Defines a test target that is always skipped.
This is useful for tests that should be skipped if some condition,
determinable during the loading phase, isn't met. The resulting target will
show up as "SKIPPED" in the output.
If possible, prefer to use `target_compatible_with` to mark tests as
incompatible. This avoids confusing behavior where the type of a target
varies depending on loading-phase behavior.
Args:
name: The name of the target.
"""
_skip_test(
name = name,
target_compatible_with = ["@platforms//:incompatible"],
tags = _SKIP_CI_TAGS,
)
def _skip_test_impl(ctx):
_ = ctx # @unused
fail("Should have been skipped")
_skip_test = rule(
implementation = _skip_test_impl,
test = True,
)
def _force_exec_config_impl(ctx):
return [DefaultInfo(
files = depset(ctx.files.tools),
default_runfiles = ctx.runfiles().merge_all([
t[DefaultInfo].default_runfiles
for t in ctx.attr.tools
]),
data_runfiles = ctx.runfiles().merge_all([
t[DefaultInfo].data_runfiles
for t in ctx.attr.tools
]),
)]
force_exec_config = rule(
implementation = _force_exec_config_impl,
doc = "Rule to force arbitrary targets to `cfg=exec` so they can be " +
"tested when used as tools.",
attrs = {
"tools": attr.label_list(
cfg = "exec",
allow_files = True,
doc = "A list of tools to force into the exec config",
),
},
)
util = struct(
# keep sorted start
empty_file = empty_file,
force_exec_config = force_exec_config,
helper_target = helper_target,
merge_kwargs = merge_kwargs,
recursive_testing_aspect = recursive_testing_aspect,
runfiles_map = runfiles_map,
runfiles_paths = runfiles_paths,
short_paths = short_paths,
skip_test = skip_test,
testing_aspect = testing_aspect,
# keep sorted end
)