xref: /aosp_15_r20/external/bazelbuild-rules_python/tests/base_rules/py_executable_base_tests.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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"""Tests common to py_binary and py_test (executable rules)."""
15
16load("@rules_python//python:py_runtime_info.bzl", RulesPythonPyRuntimeInfo = "PyRuntimeInfo")
17load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
18load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
19load("@rules_testing//lib:truth.bzl", "matching")
20load("@rules_testing//lib:util.bzl", rt_util = "util")
21load("//python:py_executable_info.bzl", "PyExecutableInfo")
22load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")  # buildifier: disable=bzl-visibility
23load("//tests/base_rules:base_tests.bzl", "create_base_tests")
24load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util")
25load("//tests/support:py_executable_info_subject.bzl", "PyExecutableInfoSubject")
26load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "LINUX_X86_64", "WINDOWS_X86_64")
27
28_BuiltinPyRuntimeInfo = PyRuntimeInfo
29
30_tests = []
31
32def _test_basic_windows(name, config):
33    if rp_config.enable_pystar:
34        target_compatible_with = []
35    else:
36        target_compatible_with = ["@platforms//:incompatible"]
37    rt_util.helper_target(
38        config.rule,
39        name = name + "_subject",
40        srcs = ["main.py"],
41        main = "main.py",
42    )
43    analysis_test(
44        name = name,
45        impl = _test_basic_windows_impl,
46        target = name + "_subject",
47        config_settings = {
48            # NOTE: The default for this flag is based on the Bazel host OS, not
49            # the target platform. For windows, it defaults to true, so force
50            # it to that to match behavior when this test runs on other
51            # platforms.
52            "//command_line_option:build_python_zip": "true",
53            "//command_line_option:cpu": "windows_x86_64",
54            "//command_line_option:crosstool_top": CROSSTOOL_TOP,
55            "//command_line_option:extra_toolchains": [CC_TOOLCHAIN],
56            "//command_line_option:platforms": [WINDOWS_X86_64],
57        },
58        attr_values = {"target_compatible_with": target_compatible_with},
59    )
60
61def _test_basic_windows_impl(env, target):
62    target = env.expect.that_target(target)
63    target.executable().path().contains(".exe")
64    target.runfiles().contains_predicate(matching.str_endswith(
65        target.meta.format_str("/{name}.zip"),
66    ))
67    target.runfiles().contains_predicate(matching.str_endswith(
68        target.meta.format_str("/{name}.exe"),
69    ))
70
71_tests.append(_test_basic_windows)
72
73def _test_basic_zip(name, config):
74    if rp_config.enable_pystar:
75        target_compatible_with = select({
76            # Disable the new test on windows because we have _test_basic_windows.
77            "@platforms//os:windows": ["@platforms//:incompatible"],
78            "//conditions:default": [],
79        })
80    else:
81        target_compatible_with = ["@platforms//:incompatible"]
82    rt_util.helper_target(
83        config.rule,
84        name = name + "_subject",
85        srcs = ["main.py"],
86        main = "main.py",
87    )
88    analysis_test(
89        name = name,
90        impl = _test_basic_zip_impl,
91        target = name + "_subject",
92        config_settings = {
93            # NOTE: The default for this flag is based on the Bazel host OS, not
94            # the target platform. For windows, it defaults to true, so force
95            # it to that to match behavior when this test runs on other
96            # platforms.
97            "//command_line_option:build_python_zip": "true",
98            "//command_line_option:cpu": "linux_x86_64",
99            "//command_line_option:crosstool_top": CROSSTOOL_TOP,
100            "//command_line_option:extra_toolchains": [CC_TOOLCHAIN],
101            "//command_line_option:platforms": [LINUX_X86_64],
102        },
103        attr_values = {"target_compatible_with": target_compatible_with},
104    )
105
106def _test_basic_zip_impl(env, target):
107    target = env.expect.that_target(target)
108    target.runfiles().contains_predicate(matching.str_endswith(
109        target.meta.format_str("/{name}.zip"),
110    ))
111    target.runfiles().contains_predicate(matching.str_endswith(
112        target.meta.format_str("/{name}"),
113    ))
114
115_tests.append(_test_basic_zip)
116
117def _test_executable_in_runfiles(name, config):
118    rt_util.helper_target(
119        config.rule,
120        name = name + "_subject",
121        srcs = [name + "_subject.py"],
122    )
123    analysis_test(
124        name = name,
125        impl = _test_executable_in_runfiles_impl,
126        target = name + "_subject",
127        attrs = WINDOWS_ATTR,
128    )
129
130_tests.append(_test_executable_in_runfiles)
131
132def _test_executable_in_runfiles_impl(env, target):
133    if pt_util.is_windows(env):
134        exe = ".exe"
135    else:
136        exe = ""
137    env.expect.that_target(target).runfiles().contains_at_least([
138        "{workspace}/{package}/{test_name}_subject" + exe,
139    ])
140
141    if rp_config.enable_pystar:
142        py_exec_info = env.expect.that_target(target).provider(PyExecutableInfo, factory = PyExecutableInfoSubject.new)
143        py_exec_info.main().path().contains("_subject.py")
144        py_exec_info.interpreter_path().contains("python")
145        py_exec_info.runfiles_without_exe().contains_none_of([
146            "{workspace}/{package}/{test_name}_subject" + exe,
147            "{workspace}/{package}/{test_name}_subject",
148        ])
149
150def _test_default_main_can_be_generated(name, config):
151    rt_util.helper_target(
152        config.rule,
153        name = name + "_subject",
154        srcs = [rt_util.empty_file(name + "_subject.py")],
155    )
156    analysis_test(
157        name = name,
158        impl = _test_default_main_can_be_generated_impl,
159        target = name + "_subject",
160    )
161
162_tests.append(_test_default_main_can_be_generated)
163
164def _test_default_main_can_be_generated_impl(env, target):
165    env.expect.that_target(target).default_outputs().contains(
166        "{package}/{test_name}_subject.py",
167    )
168
169def _test_default_main_can_have_multiple_path_segments(name, config):
170    rt_util.helper_target(
171        config.rule,
172        name = name + "/subject",
173        srcs = [name + "/subject.py"],
174    )
175    analysis_test(
176        name = name,
177        impl = _test_default_main_can_have_multiple_path_segments_impl,
178        target = name + "/subject",
179    )
180
181_tests.append(_test_default_main_can_have_multiple_path_segments)
182
183def _test_default_main_can_have_multiple_path_segments_impl(env, target):
184    env.expect.that_target(target).default_outputs().contains(
185        "{package}/{test_name}/subject.py",
186    )
187
188def _test_default_main_must_be_in_srcs(name, config):
189    # Bazel 5 will crash with a Java stacktrace when the native Python
190    # rules have an error.
191    if not pt_util.is_bazel_6_or_higher():
192        rt_util.skip_test(name = name)
193        return
194    rt_util.helper_target(
195        config.rule,
196        name = name + "_subject",
197        srcs = ["other.py"],
198    )
199    analysis_test(
200        name = name,
201        impl = _test_default_main_must_be_in_srcs_impl,
202        target = name + "_subject",
203        expect_failure = True,
204    )
205
206_tests.append(_test_default_main_must_be_in_srcs)
207
208def _test_default_main_must_be_in_srcs_impl(env, target):
209    env.expect.that_target(target).failures().contains_predicate(
210        matching.str_matches("default*does not appear in srcs"),
211    )
212
213def _test_default_main_cannot_be_ambiguous(name, config):
214    # Bazel 5 will crash with a Java stacktrace when the native Python
215    # rules have an error.
216    if not pt_util.is_bazel_6_or_higher():
217        rt_util.skip_test(name = name)
218        return
219    rt_util.helper_target(
220        config.rule,
221        name = name + "_subject",
222        srcs = [name + "_subject.py", "other/{}_subject.py".format(name)],
223    )
224    analysis_test(
225        name = name,
226        impl = _test_default_main_cannot_be_ambiguous_impl,
227        target = name + "_subject",
228        expect_failure = True,
229    )
230
231_tests.append(_test_default_main_cannot_be_ambiguous)
232
233def _test_default_main_cannot_be_ambiguous_impl(env, target):
234    env.expect.that_target(target).failures().contains_predicate(
235        matching.str_matches("default main*matches multiple files"),
236    )
237
238def _test_explicit_main(name, config):
239    rt_util.helper_target(
240        config.rule,
241        name = name + "_subject",
242        srcs = ["custom.py"],
243        main = "custom.py",
244    )
245    analysis_test(
246        name = name,
247        impl = _test_explicit_main_impl,
248        target = name + "_subject",
249    )
250
251_tests.append(_test_explicit_main)
252
253def _test_explicit_main_impl(env, target):
254    # There isn't a direct way to ask what main file was selected, so we
255    # rely on it being in the default outputs.
256    env.expect.that_target(target).default_outputs().contains(
257        "{package}/custom.py",
258    )
259
260def _test_explicit_main_cannot_be_ambiguous(name, config):
261    # Bazel 5 will crash with a Java stacktrace when the native Python
262    # rules have an error.
263    if not pt_util.is_bazel_6_or_higher():
264        rt_util.skip_test(name = name)
265        return
266    rt_util.helper_target(
267        config.rule,
268        name = name + "_subject",
269        srcs = ["x/foo.py", "y/foo.py"],
270        main = "foo.py",
271    )
272    analysis_test(
273        name = name,
274        impl = _test_explicit_main_cannot_be_ambiguous_impl,
275        target = name + "_subject",
276        expect_failure = True,
277    )
278
279_tests.append(_test_explicit_main_cannot_be_ambiguous)
280
281def _test_explicit_main_cannot_be_ambiguous_impl(env, target):
282    env.expect.that_target(target).failures().contains_predicate(
283        matching.str_matches("foo.py*matches multiple"),
284    )
285
286def _test_files_to_build(name, config):
287    rt_util.helper_target(
288        config.rule,
289        name = name + "_subject",
290        srcs = [name + "_subject.py"],
291    )
292    analysis_test(
293        name = name,
294        impl = _test_files_to_build_impl,
295        target = name + "_subject",
296        attrs = WINDOWS_ATTR,
297    )
298
299_tests.append(_test_files_to_build)
300
301def _test_files_to_build_impl(env, target):
302    default_outputs = env.expect.that_target(target).default_outputs()
303    if pt_util.is_windows(env):
304        default_outputs.contains("{package}/{test_name}_subject.exe")
305    else:
306        default_outputs.contains_exactly([
307            "{package}/{test_name}_subject",
308            "{package}/{test_name}_subject.py",
309        ])
310
311        if IS_BAZEL_7_OR_HIGHER:
312            # As of Bazel 7, the first default output is the executable, so
313            # verify that is the case. rules_testing
314            # DepsetFileSubject.contains_exactly doesn't provide an in_order()
315            # call, nor access to the underlying depset, so we have to do things
316            # manually.
317            first_default_output = target[DefaultInfo].files.to_list()[0]
318            executable = target[DefaultInfo].files_to_run.executable
319            env.expect.that_file(first_default_output).equals(executable)
320
321def _test_name_cannot_end_in_py(name, config):
322    # Bazel 5 will crash with a Java stacktrace when the native Python
323    # rules have an error.
324    if not pt_util.is_bazel_6_or_higher():
325        rt_util.skip_test(name = name)
326        return
327    rt_util.helper_target(
328        config.rule,
329        name = name + "_subject.py",
330        srcs = ["main.py"],
331    )
332    analysis_test(
333        name = name,
334        impl = _test_name_cannot_end_in_py_impl,
335        target = name + "_subject.py",
336        expect_failure = True,
337    )
338
339_tests.append(_test_name_cannot_end_in_py)
340
341def _test_name_cannot_end_in_py_impl(env, target):
342    env.expect.that_target(target).failures().contains_predicate(
343        matching.str_matches("name must not end in*.py"),
344    )
345
346def _test_py_runtime_info_provided(name, config):
347    rt_util.helper_target(
348        config.rule,
349        name = name + "_subject",
350        srcs = [name + "_subject.py"],
351    )
352    analysis_test(
353        name = name,
354        impl = _test_py_runtime_info_provided_impl,
355        target = name + "_subject",
356    )
357
358def _test_py_runtime_info_provided_impl(env, target):
359    # Make sure that the rules_python loaded symbol is provided.
360    env.expect.that_target(target).has_provider(RulesPythonPyRuntimeInfo)
361
362    # For compatibility during the transition, the builtin PyRuntimeInfo should
363    # also be provided.
364    env.expect.that_target(target).has_provider(_BuiltinPyRuntimeInfo)
365
366_tests.append(_test_py_runtime_info_provided)
367
368# Can't test this -- mandatory validation happens before analysis test
369# can intercept it
370# TODO(#1069): Once re-implemented in Starlark, modify rule logic to make this
371# testable.
372# def _test_srcs_is_mandatory(name, config):
373#     rt_util.helper_target(
374#         config.rule,
375#         name = name + "_subject",
376#     )
377#     analysis_test(
378#         name = name,
379#         impl = _test_srcs_is_mandatory,
380#         target = name + "_subject",
381#         expect_failure = True,
382#     )
383#
384# _tests.append(_test_srcs_is_mandatory)
385#
386# def _test_srcs_is_mandatory_impl(env, target):
387#     env.expect.that_target(target).failures().contains_predicate(
388#         matching.str_matches("mandatory*srcs"),
389#     )
390
391# =====
392# You were gonna add a test at the end, weren't you?
393# Nope. Please keep them sorted; put it in its alphabetical location.
394# Here's the alphabet so you don't have to sing that song in your head:
395# A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
396# =====
397
398def create_executable_tests(config):
399    def _executable_with_srcs_wrapper(name, **kwargs):
400        if not kwargs.get("srcs"):
401            kwargs["srcs"] = [name + ".py"]
402        config.rule(name = name, **kwargs)
403
404    config = pt_util.struct_with(config, base_test_rule = _executable_with_srcs_wrapper)
405    return pt_util.create_tests(_tests, config = config) + create_base_tests(config = config)
406