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("@pythons_hub//:versions.bzl", "MINOR_MAPPING") 18load("@rules_testing//lib:test_suite.bzl", "test_suite") 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.12": "3.12.4", # The `minor_mapping` will be overriden only for the missing keys 455 "3.13": "3.13.0", 456 }) 457 env.expect.that_collection(py.toolchains).contains_exactly([ 458 struct( 459 name = "python_3_13", 460 python_version = "3.13", 461 register_coverage_tool = False, 462 ), 463 ]) 464 465_tests.append(_test_add_new_version) 466 467def _test_register_all_versions(env): 468 py = parse_modules( 469 module_ctx = _mock_mctx( 470 _mod( 471 name = "my_module", 472 toolchain = [_toolchain("3.13")], 473 single_version_override = [ 474 _single_version_override( 475 python_version = "3.13.0", 476 sha256 = { 477 "aarch64-unknown-linux-gnu": "deadbeef", 478 }, 479 urls = ["example.org"], 480 ), 481 ], 482 single_version_platform_override = [ 483 _single_version_platform_override( 484 sha256 = "deadb00f", 485 urls = ["something.org"], 486 platform = "aarch64-unknown-linux-gnu", 487 python_version = "3.13.1", 488 ), 489 ], 490 override = [ 491 _override( 492 base_url = "", 493 available_python_versions = ["3.12.4", "3.13.0", "3.13.1"], 494 register_all_versions = True, 495 ), 496 ], 497 ), 498 ), 499 ) 500 501 env.expect.that_str(py.default_python_version).equals("3.13") 502 env.expect.that_collection(py.config.default["tool_versions"].keys()).contains_exactly([ 503 "3.12.4", 504 "3.13.0", 505 "3.13.1", 506 ]) 507 env.expect.that_dict(py.config.minor_mapping).contains_exactly({ 508 # The mapping is calculated automatically 509 "3.12": "3.12.4", 510 "3.13": "3.13.1", 511 }) 512 env.expect.that_collection(py.toolchains).contains_exactly([ 513 struct( 514 name = name, 515 python_version = version, 516 register_coverage_tool = False, 517 ) 518 for name, version in { 519 "python_3_12": "3.12", 520 "python_3_12_4": "3.12.4", 521 "python_3_13": "3.13", 522 "python_3_13_0": "3.13.0", 523 "python_3_13_1": "3.13.1", 524 }.items() 525 ]) 526 527_tests.append(_test_register_all_versions) 528 529def _test_add_patches(env): 530 py = parse_modules( 531 module_ctx = _mock_mctx( 532 _mod( 533 name = "my_module", 534 toolchain = [_toolchain("3.13")], 535 single_version_override = [ 536 _single_version_override( 537 python_version = "3.13.0", 538 sha256 = { 539 "aarch64-apple-darwin": "deadbeef", 540 "aarch64-unknown-linux-gnu": "deadbeef", 541 }, 542 urls = ["example.org"], 543 patch_strip = 1, 544 patches = ["common.txt"], 545 strip_prefix = "prefix", 546 distutils_content = "", 547 distutils = None, 548 ), 549 ], 550 single_version_platform_override = [ 551 _single_version_platform_override( 552 sha256 = "deadb00f", 553 urls = ["something.org", "else.org"], 554 strip_prefix = "python", 555 platform = "aarch64-unknown-linux-gnu", 556 coverage_tool = "specific_cov_tool", 557 python_version = "3.13.0", 558 patch_strip = 2, 559 patches = ["specific-patch.txt"], 560 ), 561 ], 562 override = [ 563 _override( 564 base_url = "", 565 available_python_versions = ["3.13.0"], 566 minor_mapping = { 567 "3.13": "3.13.0", 568 }, 569 ), 570 ], 571 ), 572 ), 573 ) 574 575 env.expect.that_str(py.default_python_version).equals("3.13") 576 env.expect.that_dict(py.config.default["tool_versions"]).contains_exactly({ 577 "3.13.0": { 578 "coverage_tool": {"aarch64-unknown-linux-gnu": "specific_cov_tool"}, 579 "patch_strip": {"aarch64-apple-darwin": 1, "aarch64-unknown-linux-gnu": 2}, 580 "patches": { 581 "aarch64-apple-darwin": ["common.txt"], 582 "aarch64-unknown-linux-gnu": ["specific-patch.txt"], 583 }, 584 "sha256": {"aarch64-apple-darwin": "deadbeef", "aarch64-unknown-linux-gnu": "deadb00f"}, 585 "strip_prefix": {"aarch64-apple-darwin": "prefix", "aarch64-unknown-linux-gnu": "python"}, 586 "url": { 587 "aarch64-apple-darwin": ["example.org"], 588 "aarch64-unknown-linux-gnu": ["something.org", "else.org"], 589 }, 590 }, 591 }) 592 env.expect.that_dict(py.config.minor_mapping).contains_exactly({ 593 "3.13": "3.13.0", 594 }) 595 env.expect.that_collection(py.toolchains).contains_exactly([ 596 struct( 597 name = "python_3_13", 598 python_version = "3.13", 599 register_coverage_tool = False, 600 ), 601 ]) 602 603_tests.append(_test_add_patches) 604 605def _test_fail_two_overrides(env): 606 errors = [] 607 parse_modules( 608 module_ctx = _mock_mctx( 609 _mod( 610 name = "my_module", 611 toolchain = [_toolchain("3.13")], 612 override = [ 613 _override(base_url = "foo"), 614 _override(base_url = "bar"), 615 ], 616 ), 617 ), 618 _fail = errors.append, 619 ) 620 env.expect.that_collection(errors).contains_exactly([ 621 "Only a single 'python.override' can be present", 622 ]) 623 624_tests.append(_test_fail_two_overrides) 625 626def _test_single_version_override_errors(env): 627 for test in [ 628 struct( 629 overrides = [ 630 _single_version_override(python_version = "3.12.4", distutils_content = "foo"), 631 _single_version_override(python_version = "3.12.4", distutils_content = "foo"), 632 ], 633 want_error = "Only a single 'python.single_version_override' can be present for '3.12.4'", 634 ), 635 struct( 636 overrides = [ 637 _single_version_override(python_version = "3.12.4+3", distutils_content = "foo"), 638 ], 639 want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12.4+3'", 640 ), 641 ]: 642 errors = [] 643 parse_modules( 644 module_ctx = _mock_mctx( 645 _mod( 646 name = "my_module", 647 toolchain = [_toolchain("3.13")], 648 single_version_override = test.overrides, 649 ), 650 ), 651 _fail = errors.append, 652 ) 653 env.expect.that_collection(errors).contains_exactly([test.want_error]) 654 655_tests.append(_test_single_version_override_errors) 656 657def _test_single_version_platform_override_errors(env): 658 for test in [ 659 struct( 660 overrides = [ 661 _single_version_platform_override(python_version = "3.12.4", platform = "foo", coverage_tool = "foo"), 662 _single_version_platform_override(python_version = "3.12.4", platform = "foo", coverage_tool = "foo"), 663 ], 664 want_error = "Only a single 'python.single_version_platform_override' can be present for '(\"3.12.4\", \"foo\")'", 665 ), 666 struct( 667 overrides = [ 668 _single_version_platform_override(python_version = "3.12", platform = "foo"), 669 ], 670 want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12'", 671 ), 672 struct( 673 overrides = [ 674 _single_version_platform_override(python_version = "3.12.1+my_build", platform = "foo"), 675 ], 676 want_error = "The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '3.12.1+my_build'", 677 ), 678 ]: 679 errors = [] 680 parse_modules( 681 module_ctx = _mock_mctx( 682 _mod( 683 name = "my_module", 684 toolchain = [_toolchain("3.13")], 685 single_version_platform_override = test.overrides, 686 ), 687 ), 688 _fail = errors.append, 689 ) 690 env.expect.that_collection(errors).contains_exactly([test.want_error]) 691 692_tests.append(_test_single_version_platform_override_errors) 693 694# TODO @aignas 2024-09-03: add failure tests: 695# * incorrect platform failure 696# * missing python_version failure 697 698def python_test_suite(name): 699 """Create the test suite. 700 701 Args: 702 name: the name of the test suite 703 """ 704 test_suite(name = name, basic_tests = _tests) 705