• 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_testing//lib:analysis_test.bzl", "analysis_test")
17load("@rules_testing//lib:truth.bzl", "matching")
18load("@rules_testing//lib:util.bzl", rt_util = "util")
19load("//tools/build_defs/python/tests:base_tests.bzl", "create_base_tests")
20load("//tools/build_defs/python/tests:util.bzl", "WINDOWS_ATTR", pt_util = "util")
21
22_tests = []
23
24def _test_executable_in_runfiles(name, config):
25    rt_util.helper_target(
26        config.rule,
27        name = name + "_subject",
28        srcs = [name + "_subject.py"],
29    )
30    analysis_test(
31        name = name,
32        impl = _test_executable_in_runfiles_impl,
33        target = name + "_subject",
34        attrs = WINDOWS_ATTR,
35    )
36
37_tests.append(_test_executable_in_runfiles)
38
39def _test_executable_in_runfiles_impl(env, target):
40    if pt_util.is_windows(env):
41        exe = ".exe"
42    else:
43        exe = ""
44
45    env.expect.that_target(target).runfiles().contains_at_least([
46        "{workspace}/{package}/{test_name}_subject" + exe,
47    ])
48
49def _test_default_main_can_be_generated(name, config):
50    rt_util.helper_target(
51        config.rule,
52        name = name + "_subject",
53        srcs = [rt_util.empty_file(name + "_subject.py")],
54    )
55    analysis_test(
56        name = name,
57        impl = _test_default_main_can_be_generated_impl,
58        target = name + "_subject",
59    )
60
61_tests.append(_test_default_main_can_be_generated)
62
63def _test_default_main_can_be_generated_impl(env, target):
64    env.expect.that_target(target).default_outputs().contains(
65        "{package}/{test_name}_subject.py",
66    )
67
68def _test_default_main_can_have_multiple_path_segments(name, config):
69    rt_util.helper_target(
70        config.rule,
71        name = name + "/subject",
72        srcs = [name + "/subject.py"],
73    )
74    analysis_test(
75        name = name,
76        impl = _test_default_main_can_have_multiple_path_segments_impl,
77        target = name + "/subject",
78    )
79
80_tests.append(_test_default_main_can_have_multiple_path_segments)
81
82def _test_default_main_can_have_multiple_path_segments_impl(env, target):
83    env.expect.that_target(target).default_outputs().contains(
84        "{package}/{test_name}/subject.py",
85    )
86
87def _test_default_main_must_be_in_srcs(name, config):
88    # Bazel 5 will crash with a Java stacktrace when the native Python
89    # rules have an error.
90    if not pt_util.is_bazel_6_or_higher():
91        rt_util.skip_test(name = name)
92        return
93    rt_util.helper_target(
94        config.rule,
95        name = name + "_subject",
96        srcs = ["other.py"],
97    )
98    analysis_test(
99        name = name,
100        impl = _test_default_main_must_be_in_srcs_impl,
101        target = name + "_subject",
102        expect_failure = True,
103    )
104
105_tests.append(_test_default_main_must_be_in_srcs)
106
107def _test_default_main_must_be_in_srcs_impl(env, target):
108    env.expect.that_target(target).failures().contains_predicate(
109        matching.str_matches("default*does not appear in srcs"),
110    )
111
112def _test_default_main_cannot_be_ambiguous(name, config):
113    # Bazel 5 will crash with a Java stacktrace when the native Python
114    # rules have an error.
115    if not pt_util.is_bazel_6_or_higher():
116        rt_util.skip_test(name = name)
117        return
118    rt_util.helper_target(
119        config.rule,
120        name = name + "_subject",
121        srcs = [name + "_subject.py", "other/{}_subject.py".format(name)],
122    )
123    analysis_test(
124        name = name,
125        impl = _test_default_main_cannot_be_ambiguous_impl,
126        target = name + "_subject",
127        expect_failure = True,
128    )
129
130_tests.append(_test_default_main_cannot_be_ambiguous)
131
132def _test_default_main_cannot_be_ambiguous_impl(env, target):
133    env.expect.that_target(target).failures().contains_predicate(
134        matching.str_matches("default main*matches multiple files"),
135    )
136
137def _test_explicit_main(name, config):
138    rt_util.helper_target(
139        config.rule,
140        name = name + "_subject",
141        srcs = ["custom.py"],
142        main = "custom.py",
143    )
144    analysis_test(
145        name = name,
146        impl = _test_explicit_main_impl,
147        target = name + "_subject",
148    )
149
150_tests.append(_test_explicit_main)
151
152def _test_explicit_main_impl(env, target):
153    # There isn't a direct way to ask what main file was selected, so we
154    # rely on it being in the default outputs.
155    env.expect.that_target(target).default_outputs().contains(
156        "{package}/custom.py",
157    )
158
159def _test_explicit_main_cannot_be_ambiguous(name, config):
160    # Bazel 5 will crash with a Java stacktrace when the native Python
161    # rules have an error.
162    if not pt_util.is_bazel_6_or_higher():
163        rt_util.skip_test(name = name)
164        return
165    rt_util.helper_target(
166        config.rule,
167        name = name + "_subject",
168        srcs = ["x/foo.py", "y/foo.py"],
169        main = "foo.py",
170    )
171    analysis_test(
172        name = name,
173        impl = _test_explicit_main_cannot_be_ambiguous_impl,
174        target = name + "_subject",
175        expect_failure = True,
176    )
177
178_tests.append(_test_explicit_main_cannot_be_ambiguous)
179
180def _test_explicit_main_cannot_be_ambiguous_impl(env, target):
181    env.expect.that_target(target).failures().contains_predicate(
182        matching.str_matches("foo.py*matches multiple"),
183    )
184
185def _test_files_to_build(name, config):
186    rt_util.helper_target(
187        config.rule,
188        name = name + "_subject",
189        srcs = [name + "_subject.py"],
190    )
191    analysis_test(
192        name = name,
193        impl = _test_files_to_build_impl,
194        target = name + "_subject",
195        attrs = WINDOWS_ATTR,
196    )
197
198_tests.append(_test_files_to_build)
199
200def _test_files_to_build_impl(env, target):
201    default_outputs = env.expect.that_target(target).default_outputs()
202    if pt_util.is_windows(env):
203        default_outputs.contains("{package}/{test_name}_subject.exe")
204    else:
205        default_outputs.contains_exactly([
206            "{package}/{test_name}_subject",
207            "{package}/{test_name}_subject.py",
208        ])
209
210def _test_name_cannot_end_in_py(name, config):
211    # Bazel 5 will crash with a Java stacktrace when the native Python
212    # rules have an error.
213    if not pt_util.is_bazel_6_or_higher():
214        rt_util.skip_test(name = name)
215        return
216    rt_util.helper_target(
217        config.rule,
218        name = name + "_subject.py",
219        srcs = ["main.py"],
220    )
221    analysis_test(
222        name = name,
223        impl = _test_name_cannot_end_in_py_impl,
224        target = name + "_subject.py",
225        expect_failure = True,
226    )
227
228_tests.append(_test_name_cannot_end_in_py)
229
230def _test_name_cannot_end_in_py_impl(env, target):
231    env.expect.that_target(target).failures().contains_predicate(
232        matching.str_matches("name must not end in*.py"),
233    )
234
235# Can't test this -- mandatory validation happens before analysis test
236# can intercept it
237# TODO(#1069): Once re-implemented in Starlark, modify rule logic to make this
238# testable.
239# def _test_srcs_is_mandatory(name, config):
240#     rt_util.helper_target(
241#         config.rule,
242#         name = name + "_subject",
243#     )
244#     analysis_test(
245#         name = name,
246#         impl = _test_srcs_is_mandatory,
247#         target = name + "_subject",
248#         expect_failure = True,
249#     )
250#
251# _tests.append(_test_srcs_is_mandatory)
252#
253# def _test_srcs_is_mandatory_impl(env, target):
254#     env.expect.that_target(target).failures().contains_predicate(
255#         matching.str_matches("mandatory*srcs"),
256#     )
257
258# =====
259# You were gonna add a test at the end, weren't you?
260# Nope. Please keep them sorted; put it in its alphabetical location.
261# Here's the alphabet so you don't have to sing that song in your head:
262# 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
263# =====
264
265def create_executable_tests(config):
266    def _executable_with_srcs_wrapper(name, **kwargs):
267        if not kwargs.get("srcs"):
268            kwargs["srcs"] = [name + ".py"]
269        config.rule(name = name, **kwargs)
270
271    config = pt_util.struct_with(config, base_test_rule = _executable_with_srcs_wrapper)
272    return pt_util.create_tests(_tests, config = config) + create_base_tests(config = config)
273