1# Copyright 2024 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 15"" 16 17load("@rules_testing//lib:test_suite.bzl", "test_suite") 18load("//python:versions.bzl", "MINOR_MAPPING") 19load("//python/private:python.bzl", "parse_modules") # buildifier: disable=bzl-visibility 20 21_tests = [] 22 23def _mock_mctx(*modules, environ = {}): 24 return struct( 25 os = struct(environ = environ), 26 modules = [ 27 struct( 28 name = modules[0].name, 29 tags = modules[0].tags, 30 is_root = modules[0].is_root, 31 ), 32 ] + [ 33 struct( 34 name = mod.name, 35 tags = mod.tags, 36 is_root = False, 37 ) 38 for mod in modules[1:] 39 ], 40 ) 41 42def _mod(*, name, toolchain = [], override = [], single_version_override = [], single_version_platform_override = [], is_root = True): 43 return struct( 44 name = name, 45 tags = struct( 46 toolchain = toolchain, 47 override = override, 48 single_version_override = single_version_override, 49 single_version_platform_override = single_version_platform_override, 50 ), 51 is_root = is_root, 52 ) 53 54def _toolchain(python_version, *, is_default = False, **kwargs): 55 return struct( 56 is_default = is_default, 57 python_version = python_version, 58 **kwargs 59 ) 60 61def _override( 62 auth_patterns = {}, 63 available_python_versions = [], 64 base_url = "", 65 ignore_root_user_error = False, 66 minor_mapping = {}, 67 netrc = "", 68 register_all_versions = False): 69 return struct( 70 auth_patterns = auth_patterns, 71 available_python_versions = available_python_versions, 72 base_url = base_url, 73 ignore_root_user_error = ignore_root_user_error, 74 minor_mapping = minor_mapping, 75 netrc = netrc, 76 register_all_versions = register_all_versions, 77 ) 78 79def _single_version_override( 80 python_version = "", 81 sha256 = {}, 82 urls = [], 83 patch_strip = 0, 84 patches = [], 85 strip_prefix = "python", 86 distutils_content = "", 87 distutils = None): 88 if not python_version: 89 fail("missing mandatory args: python_version ({})".format(python_version)) 90 91 return struct( 92 python_version = python_version, 93 sha256 = sha256, 94 urls = urls, 95 patch_strip = patch_strip, 96 patches = patches, 97 strip_prefix = strip_prefix, 98 distutils_content = distutils_content, 99 distutils = distutils, 100 ) 101 102def _single_version_platform_override( 103 coverage_tool = None, 104 patch_strip = 0, 105 patches = [], 106 platform = "", 107 python_version = "", 108 sha256 = "", 109 strip_prefix = "python", 110 urls = []): 111 if not platform or not python_version: 112 fail("missing mandatory args: platform ({}) and python_version ({})".format(platform, python_version)) 113 114 return struct( 115 sha256 = sha256, 116 urls = urls, 117 strip_prefix = strip_prefix, 118 platform = platform, 119 coverage_tool = coverage_tool, 120 python_version = python_version, 121 patch_strip = patch_strip, 122 patches = patches, 123 ) 124 125def _test_default(env): 126 py = parse_modules( 127 module_ctx = _mock_mctx( 128 _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), 129 ), 130 ) 131 132 # The value there should be consistent in bzlmod with the automatically 133 # calculated value Please update the MINOR_MAPPING in //python:versions.bzl 134 # when this part starts failing. 135 env.expect.that_dict(py.config.minor_mapping).contains_exactly(MINOR_MAPPING) 136 env.expect.that_collection(py.config.kwargs).has_size(0) 137 env.expect.that_collection(py.config.default.keys()).contains_exactly([ 138 "base_url", 139 "ignore_root_user_error", 140 "tool_versions", 141 ]) 142 env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(False) 143 env.expect.that_str(py.default_python_version).equals("3.11") 144 145 want_toolchain = struct( 146 name = "python_3_11", 147 python_version = "3.11", 148 register_coverage_tool = False, 149 ) 150 env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) 151 152_tests.append(_test_default) 153 154def _test_default_some_module(env): 155 py = parse_modules( 156 module_ctx = _mock_mctx( 157 _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), 158 ), 159 ) 160 161 env.expect.that_str(py.default_python_version).equals("3.11") 162 163 want_toolchain = struct( 164 name = "python_3_11", 165 python_version = "3.11", 166 register_coverage_tool = False, 167 ) 168 env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) 169 170_tests.append(_test_default_some_module) 171 172def _test_default_with_patch_version(env): 173 py = parse_modules( 174 module_ctx = _mock_mctx( 175 _mod(name = "rules_python", toolchain = [_toolchain("3.11.2")]), 176 ), 177 ) 178 179 env.expect.that_str(py.default_python_version).equals("3.11.2") 180 181 want_toolchain = struct( 182 name = "python_3_11_2", 183 python_version = "3.11.2", 184 register_coverage_tool = False, 185 ) 186 env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) 187 188_tests.append(_test_default_with_patch_version) 189 190def _test_default_non_rules_python(env): 191 py = parse_modules( 192 module_ctx = _mock_mctx( 193 # NOTE @aignas 2024-09-06: the first item in the module_ctx.modules 194 # could be a non-root module, which is the case if the root module 195 # does not make any calls to the extension. 196 _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), 197 ), 198 ) 199 200 env.expect.that_str(py.default_python_version).equals("3.11") 201 rules_python_toolchain = struct( 202 name = "python_3_11", 203 python_version = "3.11", 204 register_coverage_tool = False, 205 ) 206 env.expect.that_collection(py.toolchains).contains_exactly([rules_python_toolchain]) 207 208_tests.append(_test_default_non_rules_python) 209 210def _test_default_non_rules_python_ignore_root_user_error(env): 211 py = parse_modules( 212 module_ctx = _mock_mctx( 213 _mod( 214 name = "my_module", 215 toolchain = [_toolchain("3.12", ignore_root_user_error = True)], 216 ), 217 _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), 218 ), 219 ) 220 221 env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(True) 222 env.expect.that_str(py.default_python_version).equals("3.12") 223 224 my_module_toolchain = struct( 225 name = "python_3_12", 226 python_version = "3.12", 227 register_coverage_tool = False, 228 ) 229 rules_python_toolchain = struct( 230 name = "python_3_11", 231 python_version = "3.11", 232 register_coverage_tool = False, 233 ) 234 env.expect.that_collection(py.toolchains).contains_exactly([ 235 rules_python_toolchain, 236 my_module_toolchain, 237 ]).in_order() 238 239_tests.append(_test_default_non_rules_python_ignore_root_user_error) 240 241def _test_default_non_rules_python_ignore_root_user_error_override(env): 242 py = parse_modules( 243 module_ctx = _mock_mctx( 244 _mod( 245 name = "my_module", 246 toolchain = [_toolchain("3.12")], 247 override = [_override(ignore_root_user_error = True)], 248 ), 249 _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), 250 ), 251 ) 252 253 env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(True) 254 env.expect.that_str(py.default_python_version).equals("3.12") 255 256 my_module_toolchain = struct( 257 name = "python_3_12", 258 python_version = "3.12", 259 register_coverage_tool = False, 260 ) 261 rules_python_toolchain = struct( 262 name = "python_3_11", 263 python_version = "3.11", 264 register_coverage_tool = False, 265 ) 266 env.expect.that_collection(py.toolchains).contains_exactly([ 267 rules_python_toolchain, 268 my_module_toolchain, 269 ]).in_order() 270 271_tests.append(_test_default_non_rules_python_ignore_root_user_error_override) 272 273def _test_default_non_rules_python_ignore_root_user_error_non_root_module(env): 274 py = parse_modules( 275 module_ctx = _mock_mctx( 276 _mod(name = "my_module", toolchain = [_toolchain("3.13")]), 277 _mod(name = "some_module", toolchain = [_toolchain("3.12", ignore_root_user_error = True)]), 278 _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), 279 ), 280 ) 281 282 env.expect.that_str(py.default_python_version).equals("3.13") 283 env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(False) 284 285 my_module_toolchain = struct( 286 name = "python_3_13", 287 python_version = "3.13", 288 register_coverage_tool = False, 289 ) 290 some_module_toolchain = struct( 291 name = "python_3_12", 292 python_version = "3.12", 293 register_coverage_tool = False, 294 ) 295 rules_python_toolchain = struct( 296 name = "python_3_11", 297 python_version = "3.11", 298 register_coverage_tool = False, 299 ) 300 env.expect.that_collection(py.toolchains).contains_exactly([ 301 some_module_toolchain, 302 rules_python_toolchain, 303 my_module_toolchain, # this was the only toolchain, default to that 304 ]).in_order() 305 306_tests.append(_test_default_non_rules_python_ignore_root_user_error_non_root_module) 307 308def _test_first_occurance_of_the_toolchain_wins(env): 309 py = parse_modules( 310 module_ctx = _mock_mctx( 311 _mod(name = "my_module", toolchain = [_toolchain("3.12")]), 312 _mod(name = "some_module", toolchain = [_toolchain("3.12", configure_coverage_tool = True)]), 313 _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), 314 environ = { 315 "RULES_PYTHON_BZLMOD_DEBUG": "1", 316 }, 317 ), 318 ) 319 320 env.expect.that_str(py.default_python_version).equals("3.12") 321 322 my_module_toolchain = struct( 323 name = "python_3_12", 324 python_version = "3.12", 325 # NOTE: coverage stays disabled even though `some_module` was 326 # configuring something else. 327 register_coverage_tool = False, 328 ) 329 rules_python_toolchain = struct( 330 name = "python_3_11", 331 python_version = "3.11", 332 register_coverage_tool = False, 333 ) 334 env.expect.that_collection(py.toolchains).contains_exactly([ 335 rules_python_toolchain, 336 my_module_toolchain, # default toolchain is last 337 ]).in_order() 338 339 env.expect.that_dict(py.debug_info).contains_exactly({ 340 "toolchains_registered": [ 341 {"ignore_root_user_error": False, "module": {"is_root": True, "name": "my_module"}, "name": "python_3_12"}, 342 {"ignore_root_user_error": False, "module": {"is_root": False, "name": "rules_python"}, "name": "python_3_11"}, 343 ], 344 }) 345 346_tests.append(_test_first_occurance_of_the_toolchain_wins) 347 348def _test_auth_overrides(env): 349 py = parse_modules( 350 module_ctx = _mock_mctx( 351 _mod( 352 name = "my_module", 353 toolchain = [_toolchain("3.12")], 354 override = [ 355 _override( 356 netrc = "/my/netrc", 357 auth_patterns = {"foo": "bar"}, 358 ), 359 ], 360 ), 361 _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), 362 ), 363 ) 364 365 env.expect.that_dict(py.config.default).contains_at_least({ 366 "auth_patterns": {"foo": "bar"}, 367 "ignore_root_user_error": False, 368 "netrc": "/my/netrc", 369 }) 370 env.expect.that_str(py.default_python_version).equals("3.12") 371 372 my_module_toolchain = struct( 373 name = "python_3_12", 374 python_version = "3.12", 375 register_coverage_tool = False, 376 ) 377 rules_python_toolchain = struct( 378 name = "python_3_11", 379 python_version = "3.11", 380 register_coverage_tool = False, 381 ) 382 env.expect.that_collection(py.toolchains).contains_exactly([ 383 rules_python_toolchain, 384 my_module_toolchain, 385 ]).in_order() 386 387_tests.append(_test_auth_overrides) 388 389def _test_add_new_version(env): 390 py = parse_modules( 391 module_ctx = _mock_mctx( 392 _mod( 393 name = "my_module", 394 toolchain = [_toolchain("3.13")], 395 single_version_override = [ 396 _single_version_override( 397 python_version = "3.13.0", 398 sha256 = { 399 "aarch64-unknown-linux-gnu": "deadbeef", 400 }, 401 urls = ["example.org"], 402 patch_strip = 0, 403 patches = [], 404 strip_prefix = "prefix", 405 distutils_content = "", 406 distutils = None, 407 ), 408 ], 409 single_version_platform_override = [ 410 _single_version_platform_override( 411 sha256 = "deadb00f", 412 urls = ["something.org", "else.org"], 413 strip_prefix = "python", 414 platform = "aarch64-unknown-linux-gnu", 415 coverage_tool = "specific_cov_tool", 416 python_version = "3.13.1", 417 patch_strip = 2, 418 patches = ["specific-patch.txt"], 419 ), 420 ], 421 override = [ 422 _override( 423 base_url = "", 424 available_python_versions = ["3.12.4", "3.13.0", "3.13.1"], 425 minor_mapping = { 426 "3.13": "3.13.0", 427 }, 428 ), 429 ], 430 ), 431 ), 432 ) 433 434 env.expect.that_str(py.default_python_version).equals("3.13") 435 env.expect.that_collection(py.config.default["tool_versions"].keys()).contains_exactly([ 436 "3.12.4", 437 "3.13.0", 438 "3.13.1", 439 ]) 440 env.expect.that_dict(py.config.default["tool_versions"]["3.13.0"]).contains_exactly({ 441 "sha256": {"aarch64-unknown-linux-gnu": "deadbeef"}, 442 "strip_prefix": {"aarch64-unknown-linux-gnu": "prefix"}, 443 "url": {"aarch64-unknown-linux-gnu": ["example.org"]}, 444 }) 445 env.expect.that_dict(py.config.default["tool_versions"]["3.13.1"]).contains_exactly({ 446 "coverage_tool": {"aarch64-unknown-linux-gnu": "specific_cov_tool"}, 447 "patch_strip": {"aarch64-unknown-linux-gnu": 2}, 448 "patches": {"aarch64-unknown-linux-gnu": ["specific-patch.txt"]}, 449 "sha256": {"aarch64-unknown-linux-gnu": "deadb00f"}, 450 "strip_prefix": {"aarch64-unknown-linux-gnu": "python"}, 451 "url": {"aarch64-unknown-linux-gnu": ["something.org", "else.org"]}, 452 }) 453 env.expect.that_dict(py.config.minor_mapping).contains_exactly({ 454 "3.13": "3.13.0", 455 }) 456 env.expect.that_collection(py.toolchains).contains_exactly([ 457 struct( 458 name = "python_3_13", 459 python_version = "3.13", 460 register_coverage_tool = False, 461 ), 462 ]) 463 464_tests.append(_test_add_new_version) 465 466def _test_register_all_versions(env): 467 py = parse_modules( 468 module_ctx = _mock_mctx( 469 _mod( 470 name = "my_module", 471 toolchain = [_toolchain("3.13")], 472 single_version_override = [ 473 _single_version_override( 474 python_version = "3.13.0", 475 sha256 = { 476 "aarch64-unknown-linux-gnu": "deadbeef", 477 }, 478 urls = ["example.org"], 479 ), 480 ], 481 single_version_platform_override = [ 482 _single_version_platform_override( 483 sha256 = "deadb00f", 484 urls = ["something.org"], 485 platform = "aarch64-unknown-linux-gnu", 486 python_version = "3.13.1", 487 ), 488 ], 489 override = [ 490 _override( 491 base_url = "", 492 available_python_versions = ["3.12.4", "3.13.0", "3.13.1"], 493 register_all_versions = True, 494 ), 495 ], 496 ), 497 ), 498 ) 499 500 env.expect.that_str(py.default_python_version).equals("3.13") 501 env.expect.that_collection(py.config.default["tool_versions"].keys()).contains_exactly([ 502 "3.12.4", 503 "3.13.0", 504 "3.13.1", 505 ]) 506 env.expect.that_dict(py.config.minor_mapping).contains_exactly({ 507 # The mapping is calculated automatically 508 "3.12": "3.12.4", 509 "3.13": "3.13.1", 510 }) 511 env.expect.that_collection(py.toolchains).contains_exactly([ 512 struct( 513 name = name, 514 python_version = version, 515 register_coverage_tool = False, 516 ) 517 for name, version in { 518 "python_3_12": "3.12", 519 "python_3_12_4": "3.12.4", 520 "python_3_13": "3.13", 521 "python_3_13_0": "3.13.0", 522 "python_3_13_1": "3.13.1", 523 }.items() 524 ]) 525 526_tests.append(_test_register_all_versions) 527 528def _test_add_patches(env): 529 py = parse_modules( 530 module_ctx = _mock_mctx( 531 _mod( 532 name = "my_module", 533 toolchain = [_toolchain("3.13")], 534 single_version_override = [ 535 _single_version_override( 536 python_version = "3.13.0", 537 sha256 = { 538 "aarch64-apple-darwin": "deadbeef", 539 "aarch64-unknown-linux-gnu": "deadbeef", 540 }, 541 urls = ["example.org"], 542 patch_strip = 1, 543 patches = ["common.txt"], 544 strip_prefix = "prefix", 545 distutils_content = "", 546 distutils = None, 547 ), 548 ], 549 single_version_platform_override = [ 550 _single_version_platform_override( 551 sha256 = "deadb00f", 552 urls = ["something.org", "else.org"], 553 strip_prefix = "python", 554 platform = "aarch64-unknown-linux-gnu", 555 coverage_tool = "specific_cov_tool", 556 python_version = "3.13.0", 557 patch_strip = 2, 558 patches = ["specific-patch.txt"], 559 ), 560 ], 561 override = [ 562 _override( 563 base_url = "", 564 available_python_versions = ["3.13.0"], 565 minor_mapping = { 566 "3.13": "3.13.0", 567 }, 568 ), 569 ], 570 ), 571 ), 572 ) 573 574 env.expect.that_str(py.default_python_version).equals("3.13") 575 env.expect.that_dict(py.config.default["tool_versions"]).contains_exactly({ 576 "3.13.0": { 577 "coverage_tool": {"aarch64-unknown-linux-gnu": "specific_cov_tool"}, 578 "patch_strip": {"aarch64-apple-darwin": 1, "aarch64-unknown-linux-gnu": 2}, 579 "patches": { 580 "aarch64-apple-darwin": ["common.txt"], 581 "aarch64-unknown-linux-gnu": ["specific-patch.txt"], 582 }, 583 "sha256": {"aarch64-apple-darwin": "deadbeef", "aarch64-unknown-linux-gnu": "deadb00f"}, 584 "strip_prefix": {"aarch64-apple-darwin": "prefix", "aarch64-unknown-linux-gnu": "python"}, 585 "url": { 586 "aarch64-apple-darwin": ["example.org"], 587 "aarch64-unknown-linux-gnu": ["something.org", "else.org"], 588 }, 589 }, 590 }) 591 env.expect.that_dict(py.config.minor_mapping).contains_exactly({ 592 "3.13": "3.13.0", 593 }) 594 env.expect.that_collection(py.toolchains).contains_exactly([ 595 struct( 596 name = "python_3_13", 597 python_version = "3.13", 598 register_coverage_tool = False, 599 ), 600 ]) 601 602_tests.append(_test_add_patches) 603 604def _test_fail_two_overrides(env): 605 errors = [] 606 parse_modules( 607 module_ctx = _mock_mctx( 608 _mod( 609 name = "my_module", 610 toolchain = [_toolchain("3.13")], 611 override = [ 612 _override(base_url = "foo"), 613 _override(base_url = "bar"), 614 ], 615 ), 616 ), 617 _fail = errors.append, 618 ) 619 env.expect.that_collection(errors).contains_exactly([ 620 "Only a single 'python.override' can be present", 621 ]) 622 623_tests.append(_test_fail_two_overrides) 624 625def _test_single_version_override_errors(env): 626 for test in [ 627 struct( 628 overrides = [ 629 _single_version_override(python_version = "3.12.4", distutils_content = "foo"), 630 _single_version_override(python_version = "3.12.4", distutils_content = "foo"), 631 ], 632 want_error = "Only a single 'python.single_version_override' can be present for '3.12.4'", 633 ), 634 struct( 635 overrides = [ 636 _single_version_override(python_version = "3.12.4+3", distutils_content = "foo"), 637 ], 638 want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12.4+3'", 639 ), 640 ]: 641 errors = [] 642 parse_modules( 643 module_ctx = _mock_mctx( 644 _mod( 645 name = "my_module", 646 toolchain = [_toolchain("3.13")], 647 single_version_override = test.overrides, 648 ), 649 ), 650 _fail = errors.append, 651 ) 652 env.expect.that_collection(errors).contains_exactly([test.want_error]) 653 654_tests.append(_test_single_version_override_errors) 655 656def _test_single_version_platform_override_errors(env): 657 for test in [ 658 struct( 659 overrides = [ 660 _single_version_platform_override(python_version = "3.12.4", platform = "foo", coverage_tool = "foo"), 661 _single_version_platform_override(python_version = "3.12.4", platform = "foo", coverage_tool = "foo"), 662 ], 663 want_error = "Only a single 'python.single_version_platform_override' can be present for '(\"3.12.4\", \"foo\")'", 664 ), 665 struct( 666 overrides = [ 667 _single_version_platform_override(python_version = "3.12", platform = "foo"), 668 ], 669 want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12'", 670 ), 671 struct( 672 overrides = [ 673 _single_version_platform_override(python_version = "3.12.1+my_build", platform = "foo"), 674 ], 675 want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12.1+my_build'", 676 ), 677 ]: 678 errors = [] 679 parse_modules( 680 module_ctx = _mock_mctx( 681 _mod( 682 name = "my_module", 683 toolchain = [_toolchain("3.13")], 684 single_version_platform_override = test.overrides, 685 ), 686 ), 687 _fail = errors.append, 688 ) 689 env.expect.that_collection(errors).contains_exactly([test.want_error]) 690 691_tests.append(_test_single_version_platform_override_errors) 692 693# TODO @aignas 2024-09-03: add failure tests: 694# * incorrect platform failure 695# * missing python_version failure 696 697def python_test_suite(name): 698 """Create the test suite. 699 700 Args: 701 name: the name of the test suite 702 """ 703 test_suite(name = name, basic_tests = _tests) 704