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"""Starlark tests for py_runtime rule.""" 15 16load("@rules_python_internal//:rules_python_config.bzl", "config") 17load("@rules_testing//lib:analysis_test.bzl", "analysis_test") 18load("@rules_testing//lib:test_suite.bzl", "test_suite") 19load("@rules_testing//lib:truth.bzl", "matching") 20load("@rules_testing//lib:util.bzl", rt_util = "util") 21load("//python:py_runtime.bzl", "py_runtime") 22load("//python:py_runtime_info.bzl", "PyRuntimeInfo") 23load("//tests/base_rules:util.bzl", br_util = "util") 24load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject") 25load("//tests/support:support.bzl", "PYTHON_VERSION") 26 27_tests = [] 28 29_SKIP_TEST = { 30 "target_compatible_with": ["@platforms//:incompatible"], 31} 32 33def _simple_binary_impl(ctx): 34 executable = ctx.actions.declare_file(ctx.label.name) 35 ctx.actions.write(executable, "", is_executable = True) 36 return [DefaultInfo( 37 executable = executable, 38 files = depset([executable] + ctx.files.extra_default_outputs), 39 runfiles = ctx.runfiles(ctx.files.data), 40 )] 41 42_simple_binary = rule( 43 implementation = _simple_binary_impl, 44 attrs = { 45 "data": attr.label_list(allow_files = True), 46 "extra_default_outputs": attr.label_list(allow_files = True), 47 }, 48 executable = True, 49) 50 51def _test_bootstrap_template(name): 52 # The bootstrap_template arg isn't present in older Bazel versions, so 53 # we have to conditionally pass the arg and mark the test incompatible. 54 if config.enable_pystar: 55 py_runtime_kwargs = {"bootstrap_template": "bootstrap.txt"} 56 attr_values = {} 57 else: 58 py_runtime_kwargs = {} 59 attr_values = _SKIP_TEST 60 61 rt_util.helper_target( 62 py_runtime, 63 name = name + "_subject", 64 interpreter_path = "/py", 65 python_version = "PY3", 66 **py_runtime_kwargs 67 ) 68 analysis_test( 69 name = name, 70 target = name + "_subject", 71 impl = _test_bootstrap_template_impl, 72 attr_values = attr_values, 73 ) 74 75def _test_bootstrap_template_impl(env, target): 76 env.expect.that_target(target).provider( 77 PyRuntimeInfo, 78 factory = py_runtime_info_subject, 79 ).bootstrap_template().path().contains("bootstrap.txt") 80 81_tests.append(_test_bootstrap_template) 82 83def _test_cannot_have_both_inbuild_and_system_interpreter(name): 84 if br_util.is_bazel_6_or_higher(): 85 py_runtime_kwargs = { 86 "interpreter": "fake_interpreter", 87 "interpreter_path": "/some/path", 88 } 89 attr_values = {} 90 else: 91 py_runtime_kwargs = { 92 "interpreter_path": "/some/path", 93 } 94 attr_values = _SKIP_TEST 95 rt_util.helper_target( 96 py_runtime, 97 name = name + "_subject", 98 python_version = "PY3", 99 **py_runtime_kwargs 100 ) 101 analysis_test( 102 name = name, 103 target = name + "_subject", 104 impl = _test_cannot_have_both_inbuild_and_system_interpreter_impl, 105 expect_failure = True, 106 attr_values = attr_values, 107 ) 108 109def _test_cannot_have_both_inbuild_and_system_interpreter_impl(env, target): 110 env.expect.that_target(target).failures().contains_predicate( 111 matching.str_matches("one of*interpreter*interpreter_path"), 112 ) 113 114_tests.append(_test_cannot_have_both_inbuild_and_system_interpreter) 115 116def _test_cannot_specify_files_for_system_interpreter(name): 117 if br_util.is_bazel_6_or_higher(): 118 py_runtime_kwargs = {"files": ["foo.txt"]} 119 attr_values = {} 120 else: 121 py_runtime_kwargs = {} 122 attr_values = _SKIP_TEST 123 rt_util.helper_target( 124 py_runtime, 125 name = name + "_subject", 126 interpreter_path = "/foo", 127 python_version = "PY3", 128 **py_runtime_kwargs 129 ) 130 analysis_test( 131 name = name, 132 target = name + "_subject", 133 impl = _test_cannot_specify_files_for_system_interpreter_impl, 134 expect_failure = True, 135 attr_values = attr_values, 136 ) 137 138def _test_cannot_specify_files_for_system_interpreter_impl(env, target): 139 env.expect.that_target(target).failures().contains_predicate( 140 matching.str_matches("files*must be empty"), 141 ) 142 143_tests.append(_test_cannot_specify_files_for_system_interpreter) 144 145def _test_coverage_tool_executable(name): 146 if br_util.is_bazel_6_or_higher(): 147 py_runtime_kwargs = { 148 "coverage_tool": name + "_coverage_tool", 149 } 150 attr_values = {} 151 else: 152 py_runtime_kwargs = {} 153 attr_values = _SKIP_TEST 154 155 rt_util.helper_target( 156 py_runtime, 157 name = name + "_subject", 158 python_version = "PY3", 159 interpreter_path = "/bogus", 160 **py_runtime_kwargs 161 ) 162 rt_util.helper_target( 163 _simple_binary, 164 name = name + "_coverage_tool", 165 data = ["coverage_file1.txt", "coverage_file2.txt"], 166 ) 167 analysis_test( 168 name = name, 169 target = name + "_subject", 170 impl = _test_coverage_tool_executable_impl, 171 attr_values = attr_values, 172 ) 173 174def _test_coverage_tool_executable_impl(env, target): 175 info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject) 176 info.coverage_tool().short_path_equals("{package}/{test_name}_coverage_tool") 177 info.coverage_files().contains_exactly([ 178 "{package}/{test_name}_coverage_tool", 179 "{package}/coverage_file1.txt", 180 "{package}/coverage_file2.txt", 181 ]) 182 183_tests.append(_test_coverage_tool_executable) 184 185def _test_coverage_tool_plain_files(name): 186 if br_util.is_bazel_6_or_higher(): 187 py_runtime_kwargs = { 188 "coverage_tool": name + "_coverage_tool", 189 } 190 attr_values = {} 191 else: 192 py_runtime_kwargs = {} 193 attr_values = _SKIP_TEST 194 rt_util.helper_target( 195 py_runtime, 196 name = name + "_subject", 197 python_version = "PY3", 198 interpreter_path = "/bogus", 199 **py_runtime_kwargs 200 ) 201 rt_util.helper_target( 202 native.filegroup, 203 name = name + "_coverage_tool", 204 srcs = ["coverage_tool.py"], 205 data = ["coverage_file1.txt", "coverage_file2.txt"], 206 ) 207 analysis_test( 208 name = name, 209 target = name + "_subject", 210 impl = _test_coverage_tool_plain_files_impl, 211 attr_values = attr_values, 212 ) 213 214def _test_coverage_tool_plain_files_impl(env, target): 215 info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject) 216 info.coverage_tool().short_path_equals("{package}/coverage_tool.py") 217 info.coverage_files().contains_exactly([ 218 "{package}/coverage_tool.py", 219 "{package}/coverage_file1.txt", 220 "{package}/coverage_file2.txt", 221 ]) 222 223_tests.append(_test_coverage_tool_plain_files) 224 225def _test_in_build_interpreter(name): 226 rt_util.helper_target( 227 py_runtime, 228 name = name + "_subject", 229 interpreter = "fake_interpreter", 230 python_version = "PY3", 231 files = ["file1.txt"], 232 ) 233 analysis_test( 234 name = name, 235 target = name + "_subject", 236 impl = _test_in_build_interpreter_impl, 237 ) 238 239def _test_in_build_interpreter_impl(env, target): 240 info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject) 241 info.python_version().equals("PY3") 242 info.files().contains_predicate(matching.file_basename_equals("file1.txt")) 243 info.interpreter().path().contains("fake_interpreter") 244 245_tests.append(_test_in_build_interpreter) 246 247def _test_interpreter_binary_with_multiple_outputs(name): 248 rt_util.helper_target( 249 _simple_binary, 250 name = name + "_built_interpreter", 251 extra_default_outputs = ["extra_default_output.txt"], 252 data = ["runfile.txt"], 253 ) 254 255 rt_util.helper_target( 256 py_runtime, 257 name = name + "_subject", 258 interpreter = name + "_built_interpreter", 259 python_version = "PY3", 260 ) 261 analysis_test( 262 name = name, 263 target = name + "_subject", 264 impl = _test_interpreter_binary_with_multiple_outputs_impl, 265 ) 266 267def _test_interpreter_binary_with_multiple_outputs_impl(env, target): 268 target = env.expect.that_target(target) 269 py_runtime_info = target.provider( 270 PyRuntimeInfo, 271 factory = py_runtime_info_subject, 272 ) 273 py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter") 274 py_runtime_info.files().contains_exactly([ 275 "{package}/extra_default_output.txt", 276 "{package}/runfile.txt", 277 "{package}/{test_name}_built_interpreter", 278 ]) 279 280 target.default_outputs().contains_exactly([ 281 "{package}/extra_default_output.txt", 282 "{package}/runfile.txt", 283 "{package}/{test_name}_built_interpreter", 284 ]) 285 286 target.runfiles().contains_exactly([ 287 "{workspace}/{package}/runfile.txt", 288 "{workspace}/{package}/{test_name}_built_interpreter", 289 ]) 290 291_tests.append(_test_interpreter_binary_with_multiple_outputs) 292 293def _test_interpreter_binary_with_single_output_and_runfiles(name): 294 rt_util.helper_target( 295 _simple_binary, 296 name = name + "_built_interpreter", 297 data = ["runfile.txt"], 298 ) 299 300 rt_util.helper_target( 301 py_runtime, 302 name = name + "_subject", 303 interpreter = name + "_built_interpreter", 304 python_version = "PY3", 305 ) 306 analysis_test( 307 name = name, 308 target = name + "_subject", 309 impl = _test_interpreter_binary_with_single_output_and_runfiles_impl, 310 ) 311 312def _test_interpreter_binary_with_single_output_and_runfiles_impl(env, target): 313 target = env.expect.that_target(target) 314 py_runtime_info = target.provider( 315 PyRuntimeInfo, 316 factory = py_runtime_info_subject, 317 ) 318 py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter") 319 py_runtime_info.files().contains_exactly([ 320 "{package}/runfile.txt", 321 "{package}/{test_name}_built_interpreter", 322 ]) 323 324 target.default_outputs().contains_exactly([ 325 "{package}/runfile.txt", 326 "{package}/{test_name}_built_interpreter", 327 ]) 328 329 target.runfiles().contains_exactly([ 330 "{workspace}/{package}/runfile.txt", 331 "{workspace}/{package}/{test_name}_built_interpreter", 332 ]) 333 334_tests.append(_test_interpreter_binary_with_single_output_and_runfiles) 335 336def _test_must_have_either_inbuild_or_system_interpreter(name): 337 if br_util.is_bazel_6_or_higher(): 338 py_runtime_kwargs = {} 339 attr_values = {} 340 else: 341 py_runtime_kwargs = { 342 "interpreter_path": "/some/path", 343 } 344 attr_values = _SKIP_TEST 345 rt_util.helper_target( 346 py_runtime, 347 name = name + "_subject", 348 python_version = "PY3", 349 **py_runtime_kwargs 350 ) 351 analysis_test( 352 name = name, 353 target = name + "_subject", 354 impl = _test_must_have_either_inbuild_or_system_interpreter_impl, 355 expect_failure = True, 356 attr_values = attr_values, 357 ) 358 359def _test_must_have_either_inbuild_or_system_interpreter_impl(env, target): 360 env.expect.that_target(target).failures().contains_predicate( 361 matching.str_matches("one of*interpreter*interpreter_path"), 362 ) 363 364_tests.append(_test_must_have_either_inbuild_or_system_interpreter) 365 366def _test_system_interpreter(name): 367 rt_util.helper_target( 368 py_runtime, 369 name = name + "_subject", 370 interpreter_path = "/system/python", 371 python_version = "PY3", 372 ) 373 analysis_test( 374 name = name, 375 target = name + "_subject", 376 impl = _test_system_interpreter_impl, 377 ) 378 379def _test_system_interpreter_impl(env, target): 380 env.expect.that_target(target).provider( 381 PyRuntimeInfo, 382 factory = py_runtime_info_subject, 383 ).interpreter_path().equals("/system/python") 384 385_tests.append(_test_system_interpreter) 386 387def _test_system_interpreter_must_be_absolute(name): 388 # Bazel 5.4 will entirely crash when an invalid interpreter_path 389 # is given. 390 if br_util.is_bazel_6_or_higher(): 391 py_runtime_kwargs = {"interpreter_path": "relative/path"} 392 attr_values = {} 393 else: 394 py_runtime_kwargs = {"interpreter_path": "/junk/value/for/bazel5.4"} 395 attr_values = _SKIP_TEST 396 rt_util.helper_target( 397 py_runtime, 398 name = name + "_subject", 399 python_version = "PY3", 400 **py_runtime_kwargs 401 ) 402 analysis_test( 403 name = name, 404 target = name + "_subject", 405 impl = _test_system_interpreter_must_be_absolute_impl, 406 expect_failure = True, 407 attr_values = attr_values, 408 ) 409 410def _test_system_interpreter_must_be_absolute_impl(env, target): 411 env.expect.that_target(target).failures().contains_predicate( 412 matching.str_matches("must be*absolute"), 413 ) 414 415_tests.append(_test_system_interpreter_must_be_absolute) 416 417def _interpreter_version_info_test(name, interpreter_version_info, impl, expect_failure = True): 418 if config.enable_pystar: 419 py_runtime_kwargs = { 420 "interpreter_version_info": interpreter_version_info, 421 } 422 attr_values = {} 423 else: 424 py_runtime_kwargs = {} 425 attr_values = _SKIP_TEST 426 427 rt_util.helper_target( 428 py_runtime, 429 name = name + "_subject", 430 python_version = "PY3", 431 interpreter_path = "/py", 432 **py_runtime_kwargs 433 ) 434 analysis_test( 435 name = name, 436 target = name + "_subject", 437 impl = impl, 438 expect_failure = expect_failure, 439 attr_values = attr_values, 440 ) 441 442def _test_interpreter_version_info_must_define_major_and_minor_only_major(name): 443 _interpreter_version_info_test( 444 name, 445 { 446 "major": "3", 447 }, 448 lambda env, target: ( 449 env.expect.that_target(target).failures().contains_predicate( 450 matching.str_matches("must have at least two keys, 'major' and 'minor'"), 451 ) 452 ), 453 ) 454 455_tests.append(_test_interpreter_version_info_must_define_major_and_minor_only_major) 456 457def _test_interpreter_version_info_must_define_major_and_minor_only_minor(name): 458 _interpreter_version_info_test( 459 name, 460 { 461 "minor": "3", 462 }, 463 lambda env, target: ( 464 env.expect.that_target(target).failures().contains_predicate( 465 matching.str_matches("must have at least two keys, 'major' and 'minor'"), 466 ) 467 ), 468 ) 469 470_tests.append(_test_interpreter_version_info_must_define_major_and_minor_only_minor) 471 472def _test_interpreter_version_info_no_extraneous_keys(name): 473 _interpreter_version_info_test( 474 name, 475 { 476 "major": "3", 477 "minor": "3", 478 "something": "foo", 479 }, 480 lambda env, target: ( 481 env.expect.that_target(target).failures().contains_predicate( 482 matching.str_matches("unexpected keys [\"something\"]"), 483 ) 484 ), 485 ) 486 487_tests.append(_test_interpreter_version_info_no_extraneous_keys) 488 489def _test_interpreter_version_info_sets_values_to_none_if_not_given(name): 490 _interpreter_version_info_test( 491 name, 492 { 493 "major": "3", 494 "micro": "10", 495 "minor": "3", 496 }, 497 lambda env, target: ( 498 env.expect.that_target(target).provider( 499 PyRuntimeInfo, 500 factory = py_runtime_info_subject, 501 ).interpreter_version_info().serial().equals(None) 502 ), 503 expect_failure = False, 504 ) 505 506_tests.append(_test_interpreter_version_info_sets_values_to_none_if_not_given) 507 508def _test_interpreter_version_info_parses_values_to_struct(name): 509 _interpreter_version_info_test( 510 name, 511 { 512 "major": "3", 513 "micro": "10", 514 "minor": "6", 515 "releaselevel": "alpha", 516 "serial": "1", 517 }, 518 impl = _test_interpreter_version_info_parses_values_to_struct_impl, 519 expect_failure = False, 520 ) 521 522def _test_interpreter_version_info_parses_values_to_struct_impl(env, target): 523 version_info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject).interpreter_version_info() 524 version_info.major().equals(3) 525 version_info.minor().equals(6) 526 version_info.micro().equals(10) 527 version_info.releaselevel().equals("alpha") 528 version_info.serial().equals(1) 529 530_tests.append(_test_interpreter_version_info_parses_values_to_struct) 531 532def _test_version_info_from_flag(name): 533 if not config.enable_pystar: 534 rt_util.skip_test(name) 535 return 536 py_runtime( 537 name = name + "_subject", 538 interpreter_version_info = None, 539 interpreter_path = "/bogus", 540 ) 541 analysis_test( 542 name = name, 543 target = name + "_subject", 544 impl = _test_version_info_from_flag_impl, 545 config_settings = { 546 PYTHON_VERSION: "3.12", 547 }, 548 ) 549 550def _test_version_info_from_flag_impl(env, target): 551 version_info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject).interpreter_version_info() 552 version_info.major().equals(3) 553 version_info.minor().equals(12) 554 version_info.micro().equals(None) 555 version_info.releaselevel().equals(None) 556 version_info.serial().equals(None) 557 558_tests.append(_test_version_info_from_flag) 559 560def py_runtime_test_suite(name): 561 test_suite( 562 name = name, 563 tests = _tests, 564 ) 565