• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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:reexports.bzl", "BuiltinPyRuntimeInfo")  # buildifier: disable=bzl-visibility
23load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")  # buildifier: disable=bzl-visibility
24load("//tests/base_rules:base_tests.bzl", "create_base_tests")
25load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util")
26load("//tests/support:py_executable_info_subject.bzl", "PyExecutableInfoSubject")
27load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "LINUX_X86_64", "WINDOWS_X86_64")
28
29_tests = []
30
31def _test_basic_windows(name, config):
32    if rp_config.enable_pystar:
33        target_compatible_with = []
34    else:
35        target_compatible_with = ["@platforms//:incompatible"]
36    rt_util.helper_target(
37        config.rule,
38        name = name + "_subject",
39        srcs = ["main.py"],
40        main = "main.py",
41    )
42    analysis_test(
43        name = name,
44        impl = _test_basic_windows_impl,
45        target = name + "_subject",
46        config_settings = {
47            # NOTE: The default for this flag is based on the Bazel host OS, not
48            # the target platform. For windows, it defaults to true, so force
49            # it to that to match behavior when this test runs on other
50            # platforms.
51            "//command_line_option:build_python_zip": "true",
52            "//command_line_option:cpu": "windows_x86_64",
53            "//command_line_option:crosstool_top": CROSSTOOL_TOP,
54            "//command_line_option:extra_toolchains": [CC_TOOLCHAIN],
55            "//command_line_option:platforms": [WINDOWS_X86_64],
56        },
57        attr_values = {"target_compatible_with": target_compatible_with},
58    )
59
60def _test_basic_windows_impl(env, target):
61    target = env.expect.that_target(target)
62    target.executable().path().contains(".exe")
63    target.runfiles().contains_predicate(matching.str_endswith(
64        target.meta.format_str("/{name}.zip"),
65    ))
66    target.runfiles().contains_predicate(matching.str_endswith(
67        target.meta.format_str("/{name}.exe"),
68    ))
69
70_tests.append(_test_basic_windows)
71
72def _test_basic_zip(name, config):
73    if rp_config.enable_pystar:
74        target_compatible_with = select({
75            # Disable the new test on windows because we have _test_basic_windows.
76            "@platforms//os:windows": ["@platforms//:incompatible"],
77            "//conditions:default": [],
78        })
79    else:
80        target_compatible_with = ["@platforms//:incompatible"]
81    rt_util.helper_target(
82        config.rule,
83        name = name + "_subject",
84        srcs = ["main.py"],
85        main = "main.py",
86    )
87    analysis_test(
88        name = name,
89        impl = _test_basic_zip_impl,
90        target = name + "_subject",
91        config_settings = {
92            # NOTE: The default for this flag is based on the Bazel host OS, not
93            # the target platform. For windows, it defaults to true, so force
94            # it to that to match behavior when this test runs on other
95            # platforms.
96            "//command_line_option:build_python_zip": "true",
97            "//command_line_option:cpu": "linux_x86_64",
98            "//command_line_option:crosstool_top": CROSSTOOL_TOP,
99            "//command_line_option:extra_toolchains": [CC_TOOLCHAIN],
100            "//command_line_option:platforms": [LINUX_X86_64],
101        },
102        attr_values = {"target_compatible_with": target_compatible_with},
103    )
104
105def _test_basic_zip_impl(env, target):
106    target = env.expect.that_target(target)
107    target.runfiles().contains_predicate(matching.str_endswith(
108        target.meta.format_str("/{name}.zip"),
109    ))
110    target.runfiles().contains_predicate(matching.str_endswith(
111        target.meta.format_str("/{name}"),
112    ))
113
114_tests.append(_test_basic_zip)
115
116def _test_executable_in_runfiles(name, config):
117    rt_util.helper_target(
118        config.rule,
119        name = name + "_subject",
120        srcs = [name + "_subject.py"],
121    )
122    analysis_test(
123        name = name,
124        impl = _test_executable_in_runfiles_impl,
125        target = name + "_subject",
126        attrs = WINDOWS_ATTR,
127    )
128
129_tests.append(_test_executable_in_runfiles)
130
131def _test_executable_in_runfiles_impl(env, target):
132    if pt_util.is_windows(env):
133        exe = ".exe"
134    else:
135        exe = ""
136    env.expect.that_target(target).runfiles().contains_at_least([
137        "{workspace}/{package}/{test_name}_subject" + exe,
138    ])
139
140    if rp_config.enable_pystar:
141        py_exec_info = env.expect.that_target(target).provider(PyExecutableInfo, factory = PyExecutableInfoSubject.new)
142        py_exec_info.main().path().contains("_subject.py")
143        py_exec_info.interpreter_path().contains("python")
144        py_exec_info.runfiles_without_exe().contains_none_of([
145            "{workspace}/{package}/{test_name}_subject" + exe,
146            "{workspace}/{package}/{test_name}_subject",
147        ])
148
149def _test_default_main_can_be_generated(name, config):
150    rt_util.helper_target(
151        config.rule,
152        name = name + "_subject",
153        srcs = [rt_util.empty_file(name + "_subject.py")],
154    )
155    analysis_test(
156        name = name,
157        impl = _test_default_main_can_be_generated_impl,
158        target = name + "_subject",
159    )
160
161_tests.append(_test_default_main_can_be_generated)
162
163def _test_default_main_can_be_generated_impl(env, target):
164    env.expect.that_target(target).default_outputs().contains(
165        "{package}/{test_name}_subject.py",
166    )
167
168def _test_default_main_can_have_multiple_path_segments(name, config):
169    rt_util.helper_target(
170        config.rule,
171        name = name + "/subject",
172        srcs = [name + "/subject.py"],
173    )
174    analysis_test(
175        name = name,
176        impl = _test_default_main_can_have_multiple_path_segments_impl,
177        target = name + "/subject",
178    )
179
180_tests.append(_test_default_main_can_have_multiple_path_segments)
181
182def _test_default_main_can_have_multiple_path_segments_impl(env, target):
183    env.expect.that_target(target).default_outputs().contains(
184        "{package}/{test_name}/subject.py",
185    )
186
187def _test_default_main_must_be_in_srcs(name, config):
188    # Bazel 5 will crash with a Java stacktrace when the native Python
189    # rules have an error.
190    if not pt_util.is_bazel_6_or_higher():
191        rt_util.skip_test(name = name)
192        return
193    rt_util.helper_target(
194        config.rule,
195        name = name + "_subject",
196        srcs = ["other.py"],
197    )
198    analysis_test(
199        name = name,
200        impl = _test_default_main_must_be_in_srcs_impl,
201        target = name + "_subject",
202        expect_failure = True,
203    )
204
205_tests.append(_test_default_main_must_be_in_srcs)
206
207def _test_default_main_must_be_in_srcs_impl(env, target):
208    env.expect.that_target(target).failures().contains_predicate(
209        matching.str_matches("default*does not appear in srcs"),
210    )
211
212def _test_default_main_cannot_be_ambiguous(name, config):
213    # Bazel 5 will crash with a Java stacktrace when the native Python
214    # rules have an error.
215    if not pt_util.is_bazel_6_or_higher():
216        rt_util.skip_test(name = name)
217        return
218    rt_util.helper_target(
219        config.rule,
220        name = name + "_subject",
221        srcs = [name + "_subject.py", "other/{}_subject.py".format(name)],
222    )
223    analysis_test(
224        name = name,
225        impl = _test_default_main_cannot_be_ambiguous_impl,
226        target = name + "_subject",
227        expect_failure = True,
228    )
229
230_tests.append(_test_default_main_cannot_be_ambiguous)
231
232def _test_default_main_cannot_be_ambiguous_impl(env, target):
233    env.expect.that_target(target).failures().contains_predicate(
234        matching.str_matches("default main*matches multiple files"),
235    )
236
237def _test_explicit_main(name, config):
238    rt_util.helper_target(
239        config.rule,
240        name = name + "_subject",
241        srcs = ["custom.py"],
242        main = "custom.py",
243    )
244    analysis_test(
245        name = name,
246        impl = _test_explicit_main_impl,
247        target = name + "_subject",
248    )
249
250_tests.append(_test_explicit_main)
251
252def _test_explicit_main_impl(env, target):
253    # There isn't a direct way to ask what main file was selected, so we
254    # rely on it being in the default outputs.
255    env.expect.that_target(target).default_outputs().contains(
256        "{package}/custom.py",
257    )
258
259def _test_explicit_main_cannot_be_ambiguous(name, config):
260    # Bazel 5 will crash with a Java stacktrace when the native Python
261    # rules have an error.
262    if not pt_util.is_bazel_6_or_higher():
263        rt_util.skip_test(name = name)
264        return
265    rt_util.helper_target(
266        config.rule,
267        name = name + "_subject",
268        srcs = ["x/foo.py", "y/foo.py"],
269        main = "foo.py",
270    )
271    analysis_test(
272        name = name,
273        impl = _test_explicit_main_cannot_be_ambiguous_impl,
274        target = name + "_subject",
275        expect_failure = True,
276    )
277
278_tests.append(_test_explicit_main_cannot_be_ambiguous)
279
280def _test_explicit_main_cannot_be_ambiguous_impl(env, target):
281    env.expect.that_target(target).failures().contains_predicate(
282        matching.str_matches("foo.py*matches multiple"),
283    )
284
285def _test_files_to_build(name, config):
286    rt_util.helper_target(
287        config.rule,
288        name = name + "_subject",
289        srcs = [name + "_subject.py"],
290    )
291    analysis_test(
292        name = name,
293        impl = _test_files_to_build_impl,
294        target = name + "_subject",
295        attrs = WINDOWS_ATTR,
296    )
297
298_tests.append(_test_files_to_build)
299
300def _test_files_to_build_impl(env, target):
301    default_outputs = env.expect.that_target(target).default_outputs()
302    if pt_util.is_windows(env):
303        default_outputs.contains("{package}/{test_name}_subject.exe")
304    else:
305        default_outputs.contains_exactly([
306            "{package}/{test_name}_subject",
307            "{package}/{test_name}_subject.py",
308        ])
309
310        if IS_BAZEL_7_OR_HIGHER:
311            # As of Bazel 7, the first default output is the executable, so
312            # verify that is the case. rules_testing
313            # DepsetFileSubject.contains_exactly doesn't provide an in_order()
314            # call, nor access to the underlying depset, so we have to do things
315            # manually.
316            first_default_output = target[DefaultInfo].files.to_list()[0]
317            executable = target[DefaultInfo].files_to_run.executable
318            env.expect.that_file(first_default_output).equals(executable)
319
320def _test_name_cannot_end_in_py(name, config):
321    # Bazel 5 will crash with a Java stacktrace when the native Python
322    # rules have an error.
323    if not pt_util.is_bazel_6_or_higher():
324        rt_util.skip_test(name = name)
325        return
326    rt_util.helper_target(
327        config.rule,
328        name = name + "_subject.py",
329        srcs = ["main.py"],
330    )
331    analysis_test(
332        name = name,
333        impl = _test_name_cannot_end_in_py_impl,
334        target = name + "_subject.py",
335        expect_failure = True,
336    )
337
338_tests.append(_test_name_cannot_end_in_py)
339
340def _test_name_cannot_end_in_py_impl(env, target):
341    env.expect.that_target(target).failures().contains_predicate(
342        matching.str_matches("name must not end in*.py"),
343    )
344
345def _test_py_runtime_info_provided(name, config):
346    rt_util.helper_target(
347        config.rule,
348        name = name + "_subject",
349        srcs = [name + "_subject.py"],
350    )
351    analysis_test(
352        name = name,
353        impl = _test_py_runtime_info_provided_impl,
354        target = name + "_subject",
355    )
356
357def _test_py_runtime_info_provided_impl(env, target):
358    # Make sure that the rules_python loaded symbol is provided.
359    env.expect.that_target(target).has_provider(RulesPythonPyRuntimeInfo)
360
361    if BuiltinPyRuntimeInfo != None:
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