xref: /aosp_15_r20/external/bazelbuild-rules_python/tests/support/sh_py_run_test.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
1# Copyright 2024 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"""Run a py_binary with altered config settings in an sh_test.
15
16This facilitates verify running binaries with different configuration settings
17without the overhead of a bazel-in-bazel integration test.
18"""
19
20load("//python:py_binary.bzl", "py_binary")
21load("//python:py_test.bzl", "py_test")
22load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")  # buildifier: disable=bzl-visibility
23load("//tests/support:support.bzl", "VISIBLE_FOR_TESTING")
24
25def _perform_transition_impl(input_settings, attr):
26    settings = dict(input_settings)
27    settings[VISIBLE_FOR_TESTING] = True
28    settings["//command_line_option:build_python_zip"] = attr.build_python_zip
29    if attr.bootstrap_impl:
30        settings["//python/config_settings:bootstrap_impl"] = attr.bootstrap_impl
31    if attr.extra_toolchains:
32        settings["//command_line_option:extra_toolchains"] = attr.extra_toolchains
33    if attr.python_version:
34        settings["//python/config_settings:python_version"] = attr.python_version
35    return settings
36
37_perform_transition = transition(
38    implementation = _perform_transition_impl,
39    inputs = [
40        "//python/config_settings:bootstrap_impl",
41        "//command_line_option:extra_toolchains",
42        "//python/config_settings:python_version",
43    ],
44    outputs = [
45        "//command_line_option:build_python_zip",
46        "//command_line_option:extra_toolchains",
47        "//python/config_settings:bootstrap_impl",
48        "//python/config_settings:python_version",
49        VISIBLE_FOR_TESTING,
50    ],
51)
52
53def _py_reconfig_impl(ctx):
54    default_info = ctx.attr.target[DefaultInfo]
55    exe_ext = default_info.files_to_run.executable.extension
56    if exe_ext:
57        exe_ext = "." + exe_ext
58    exe_name = ctx.label.name + exe_ext
59
60    executable = ctx.actions.declare_file(exe_name)
61    ctx.actions.symlink(output = executable, target_file = default_info.files_to_run.executable)
62
63    default_outputs = [executable]
64
65    # todo: could probably check target.owner vs src.owner to check if it should
66    # be symlinked or included as-is
67    # For simplicity of implementation, we're assuming the target being run is
68    # py_binary-like. In order for Windows to work, we need to make sure the
69    # file that the .exe launcher runs (the .zip or underlying non-exe
70    # executable) is a sibling of the .exe file with the same base name.
71    for src in default_info.files.to_list():
72        if src.extension in ("", "zip"):
73            ext = ("." if src.extension else "") + src.extension
74            output = ctx.actions.declare_file(ctx.label.name + ext)
75            ctx.actions.symlink(output = output, target_file = src)
76            default_outputs.append(output)
77
78    return [
79        DefaultInfo(
80            executable = executable,
81            files = depset(default_outputs),
82            # On windows, the other default outputs must also be included
83            # in runfiles so the exe launcher can find the backing file.
84            runfiles = ctx.runfiles(default_outputs).merge(
85                default_info.default_runfiles,
86            ),
87        ),
88        testing.TestEnvironment(
89            environment = ctx.attr.env,
90        ),
91    ]
92
93def _make_reconfig_rule(**kwargs):
94    attrs = {
95        "bootstrap_impl": attr.string(),
96        "build_python_zip": attr.string(default = "auto"),
97        "env": attr.string_dict(),
98        "extra_toolchains": attr.string_list(
99            doc = """
100Value for the --extra_toolchains flag.
101
102NOTE: You'll likely have to also specify //tests/support/cc_toolchains:all (or some CC toolchain)
103to make the RBE presubmits happy, which disable auto-detection of a CC
104toolchain.
105""",
106        ),
107        "python_version": attr.string(),
108        "target": attr.label(executable = True, cfg = "target"),
109        "_allowlist_function_transition": attr.label(
110            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
111        ),
112    }
113    return rule(
114        implementation = _py_reconfig_impl,
115        attrs = attrs,
116        cfg = _perform_transition,
117        **kwargs
118    )
119
120_py_reconfig_binary = _make_reconfig_rule(executable = True)
121
122_py_reconfig_test = _make_reconfig_rule(test = True)
123
124def py_reconfig_test(*, name, **kwargs):
125    """Create a py_test with customized build settings for testing.
126
127    Args:
128        name: str, name of teset target.
129        **kwargs: kwargs to pass along to _py_reconfig_test and py_test.
130    """
131    reconfig_kwargs = {}
132    reconfig_kwargs["bootstrap_impl"] = kwargs.pop("bootstrap_impl", None)
133    reconfig_kwargs["extra_toolchains"] = kwargs.pop("extra_toolchains", None)
134    reconfig_kwargs["python_version"] = kwargs.pop("python_version", None)
135    reconfig_kwargs["env"] = kwargs.get("env")
136    reconfig_kwargs["target_compatible_with"] = kwargs.get("target_compatible_with")
137
138    inner_name = "_{}_inner" + name
139    _py_reconfig_test(
140        name = name,
141        target = inner_name,
142        **reconfig_kwargs
143    )
144    py_test(
145        name = inner_name,
146        tags = ["manual"],
147        **kwargs
148    )
149
150def sh_py_run_test(*, name, sh_src, py_src, **kwargs):
151    bin_name = "_{}_bin".format(name)
152    native.sh_test(
153        name = name,
154        srcs = [sh_src],
155        data = [bin_name],
156        deps = [
157            "@bazel_tools//tools/bash/runfiles",
158        ],
159        env = {
160            "BIN_RLOCATION": "$(rlocationpath {})".format(bin_name),
161        },
162    )
163
164    _py_reconfig_binary(
165        name = bin_name,
166        tags = ["manual"],
167        target = "_{}_plain_bin".format(name),
168        **kwargs
169    )
170
171    py_binary(
172        name = "_{}_plain_bin".format(name),
173        srcs = [py_src],
174        main = py_src,
175        tags = ["manual"],
176    )
177
178def _current_build_settings_impl(ctx):
179    info = ctx.actions.declare_file(ctx.label.name + ".json")
180    toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE]
181    runtime = toolchain.py3_runtime
182    files = [info]
183    ctx.actions.write(
184        output = info,
185        content = json.encode({
186            "interpreter": {
187                "short_path": runtime.interpreter.short_path if runtime.interpreter else None,
188            },
189            "interpreter_path": runtime.interpreter_path,
190            "toolchain_label": str(getattr(toolchain, "toolchain_label", None)),
191        }),
192    )
193    return [DefaultInfo(
194        files = depset(files),
195    )]
196
197current_build_settings = rule(
198    doc = """
199Writes information about the current build config to JSON for testing.
200
201This is so tests can verify information about the build config used for them.
202""",
203    implementation = _current_build_settings_impl,
204    toolchains = [
205        TARGET_TOOLCHAIN_TYPE,
206    ],
207)
208