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