blob: 7a32be9d56c17cae704802ea01367036e152b139 [file] [log] [blame]
# Copyright 2023 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Python test utilities."""
load("@bazel_skylib//lib:shell.bzl", "shell")
# Test rule definition whose actions invoke a python binary must do the following:
#
# - Merge the PY_TOOLCHAIN_DEPS dictionary to their `attr` argument to add
# required dependencies.
#
# - Call either create_python3_shell_wrapper_provider() to return
# a DefaultInfo value corresponding to the generated shell script
# and its runtime requirements.
#
# This ensures that the interpreter and all dependent files are available during
# test execution. For example:
#
# def _my_python_test_impl(ctx):
# return [
# create_python3_shell_wrapper_provider(
# ctx,
# ctx.attr.script.short_path,
# ctx.attr.script_args,
# ctx.runfiles(files = ctx.files.data),
# ),
# ]
#
# my_python_test = rule(
# implementation = _my_rule_impl,
# test = True,
# attrs = {
# "script": attr.label(
# mandatory = True,
# allow_single_file = True,
# doc = "Label to Python3 script to invoke at runtime for this test.",
# ),
# "script_args": attr.string_list(
# default = [],
# doc = "List of extra arguments to pass to the script.",
# ),
# "data": attr.label_list(
# doc = "List of extra runtime dependencies for the script."
# ),
# } | PY_TOOLCHAIN_DEPS,
# )
#
PY_TOOLCHAIN_DEPS = {
"_py_toolchain": attr.label(
default = "@rules_python//python:current_py_toolchain",
cfg = "exec",
providers = [DefaultInfo, platform_common.ToolchainInfo],
),
}
# Script template
_python3_shell_script_template = """\
#!/bin/sh
exec {py3_exec} -S {py3_script} {args} "$@"
"""
def create_python3_shell_wrapper_provider(ctx, py3_script_path, args = [], runfiles = None):
"""Create a shell script that invokes a given Python script for tests.
This function can be called from a rule implementation function, and
produces an output File instance (for the generated shell script)
and a runfiles value (for its runtime dependencies, which include
any required Python toolchain runtime files).
The rule definition must use PY_TOOLCHAIN_DEPS.
usage example:
def _my_python_test_impl(ctx):
return [
create_python3_shell_wrapper_provider(
ctx,
ctx.attr.script.short_path,
ctx.attr.script_args,
ctx.runfiles(files = ctx.files.data),
),
]
my_python_test = rule(
implementation = _my_rule_impl,
test = True,
attrs = {
"script": attr.label(
mandatory = True,
allow_single_file = True,
doc = "Label to Python3 script to invoke at runtime for this test.",
),
"script_args": attr.string_list(
default = [],
doc = "List of extra arguments to pass to the script.",
),
"data": attr.label_list(
doc = "List of extra runtime dependencies for the script."
),
} | PY_TOOLCHAIN_DEPS,
)
Args:
ctx: Rule context
py3_script_path: Short path to python script.
args: [string list] optional list of extra arguments.
runfiles: [runfiles] Optional runfiles value for files required at runtime.
Returns:
A DefaultInfo provider for the generated script and its runtime requirements.
"""
if not hasattr(ctx.attr, "_py_toolchain"):
fail("The rule calling this function must use PY_TOOLCHAIN_DEPS!")
toolchain_info = ctx.attr._py_toolchain[platform_common.ToolchainInfo]
if not toolchain_info.py3_runtime:
fail("A Bazel python3 runtime is required, and none was configured!")
python3_executable = toolchain_info.py3_runtime.interpreter
python3_runfiles = ctx.runfiles(transitive_files = toolchain_info.py3_runtime.files)
script = ctx.actions.declare_file(ctx.label.name + ".sh")
script_content = _python3_shell_script_template.format(
py3_exec = python3_executable.short_path,
py3_script = py3_script_path,
args = " ".join([shell.quote(a) for a in args]),
)
ctx.actions.write(script, script_content, is_executable = True)
if runfiles == None:
runfiles = python3_runfiles
else:
runfiles = runfiles.merge(python3_runfiles)
return DefaultInfo(
executable = script,
runfiles = runfiles,
)