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("@rules_testing//lib:truth.bzl", "subjects") 19load("//python/private/pypi:extension.bzl", "parse_modules") # buildifier: disable=bzl-visibility 20load("//python/private/pypi:whl_config_setting.bzl", "whl_config_setting") # buildifier: disable=bzl-visibility 21 22_tests = [] 23 24def _mock_mctx(*modules, environ = {}, read = None): 25 return struct( 26 os = struct( 27 environ = environ, 28 name = "unittest", 29 arch = "exotic", 30 ), 31 read = read or (lambda _: "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf"), 32 modules = [ 33 struct( 34 name = modules[0].name, 35 tags = modules[0].tags, 36 is_root = modules[0].is_root, 37 ), 38 ] + [ 39 struct( 40 name = mod.name, 41 tags = mod.tags, 42 is_root = False, 43 ) 44 for mod in modules[1:] 45 ], 46 ) 47 48def _mod(*, name, parse = [], override = [], whl_mods = [], is_root = True): 49 return struct( 50 name = name, 51 tags = struct( 52 parse = parse, 53 override = override, 54 whl_mods = whl_mods, 55 ), 56 is_root = is_root, 57 ) 58 59def _parse_modules(env, **kwargs): 60 return env.expect.that_struct( 61 parse_modules(**kwargs), 62 attrs = dict( 63 is_reproducible = subjects.bool, 64 exposed_packages = subjects.dict, 65 hub_group_map = subjects.dict, 66 hub_whl_map = subjects.dict, 67 whl_libraries = subjects.dict, 68 whl_mods = subjects.dict, 69 ), 70 ) 71 72def _parse( 73 *, 74 hub_name, 75 python_version, 76 _evaluate_markers_srcs = [], 77 auth_patterns = {}, 78 download_only = False, 79 enable_implicit_namespace_pkgs = False, 80 environment = {}, 81 envsubst = {}, 82 experimental_index_url = "", 83 experimental_requirement_cycles = {}, 84 experimental_target_platforms = [], 85 extra_hub_aliases = {}, 86 extra_pip_args = [], 87 isolated = True, 88 netrc = None, 89 parse_all_requirements_files = True, 90 pip_data_exclude = None, 91 python_interpreter = None, 92 python_interpreter_target = None, 93 quiet = True, 94 requirements_by_platform = {}, 95 requirements_darwin = None, 96 requirements_linux = None, 97 requirements_lock = None, 98 requirements_windows = None, 99 timeout = 600, 100 whl_modifications = {}, 101 **kwargs): 102 return struct( 103 _evaluate_markers_srcs = _evaluate_markers_srcs, 104 auth_patterns = auth_patterns, 105 download_only = download_only, 106 enable_implicit_namespace_pkgs = enable_implicit_namespace_pkgs, 107 environment = environment, 108 envsubst = envsubst, 109 experimental_index_url = experimental_index_url, 110 experimental_requirement_cycles = experimental_requirement_cycles, 111 experimental_target_platforms = experimental_target_platforms, 112 extra_hub_aliases = extra_hub_aliases, 113 extra_pip_args = extra_pip_args, 114 hub_name = hub_name, 115 isolated = isolated, 116 netrc = netrc, 117 parse_all_requirements_files = parse_all_requirements_files, 118 pip_data_exclude = pip_data_exclude, 119 python_interpreter = python_interpreter, 120 python_interpreter_target = python_interpreter_target, 121 python_version = python_version, 122 quiet = quiet, 123 requirements_by_platform = requirements_by_platform, 124 requirements_darwin = requirements_darwin, 125 requirements_linux = requirements_linux, 126 requirements_lock = requirements_lock, 127 requirements_windows = requirements_windows, 128 timeout = timeout, 129 whl_modifications = whl_modifications, 130 # The following are covered by other unit tests 131 experimental_extra_index_urls = [], 132 parallel_download = False, 133 experimental_index_url_overrides = {}, 134 **kwargs 135 ) 136 137def _test_simple(env): 138 pypi = _parse_modules( 139 env, 140 module_ctx = _mock_mctx( 141 _mod( 142 name = "rules_python", 143 parse = [ 144 _parse( 145 hub_name = "pypi", 146 python_version = "3.15", 147 requirements_lock = "requirements.txt", 148 ), 149 ], 150 ), 151 ), 152 available_interpreters = { 153 "python_3_15_host": "unit_test_interpreter_target", 154 }, 155 ) 156 157 pypi.is_reproducible().equals(True) 158 pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) 159 pypi.hub_group_map().contains_exactly({"pypi": {}}) 160 pypi.hub_whl_map().contains_exactly({"pypi": { 161 "simple": { 162 "pypi_315_simple": [ 163 whl_config_setting( 164 version = "3.15", 165 ), 166 ], 167 }, 168 }}) 169 pypi.whl_libraries().contains_exactly({ 170 "pypi_315_simple": { 171 "dep_template": "@pypi//{name}:{target}", 172 "python_interpreter_target": "unit_test_interpreter_target", 173 "repo": "pypi_315", 174 "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", 175 }, 176 }) 177 pypi.whl_mods().contains_exactly({}) 178 179_tests.append(_test_simple) 180 181def _test_simple_multiple_requirements(env): 182 pypi = _parse_modules( 183 env, 184 module_ctx = _mock_mctx( 185 _mod( 186 name = "rules_python", 187 parse = [ 188 _parse( 189 hub_name = "pypi", 190 python_version = "3.15", 191 requirements_darwin = "darwin.txt", 192 requirements_windows = "win.txt", 193 ), 194 ], 195 ), 196 read = lambda x: { 197 "darwin.txt": "simple==0.0.2 --hash=sha256:deadb00f", 198 "win.txt": "simple==0.0.1 --hash=sha256:deadbeef", 199 }[x], 200 ), 201 available_interpreters = { 202 "python_3_15_host": "unit_test_interpreter_target", 203 }, 204 ) 205 206 pypi.is_reproducible().equals(True) 207 pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) 208 pypi.hub_group_map().contains_exactly({"pypi": {}}) 209 pypi.hub_whl_map().contains_exactly({"pypi": { 210 "simple": { 211 "pypi_315_simple_osx_aarch64_osx_x86_64": [ 212 whl_config_setting( 213 target_platforms = [ 214 "cp315_osx_aarch64", 215 "cp315_osx_x86_64", 216 ], 217 version = "3.15", 218 ), 219 ], 220 "pypi_315_simple_windows_x86_64": [ 221 whl_config_setting( 222 target_platforms = [ 223 "cp315_windows_x86_64", 224 ], 225 version = "3.15", 226 ), 227 ], 228 }, 229 }}) 230 pypi.whl_libraries().contains_exactly({ 231 "pypi_315_simple_osx_aarch64_osx_x86_64": { 232 "dep_template": "@pypi//{name}:{target}", 233 "python_interpreter_target": "unit_test_interpreter_target", 234 "repo": "pypi_315", 235 "requirement": "simple==0.0.2 --hash=sha256:deadb00f", 236 }, 237 "pypi_315_simple_windows_x86_64": { 238 "dep_template": "@pypi//{name}:{target}", 239 "python_interpreter_target": "unit_test_interpreter_target", 240 "repo": "pypi_315", 241 "requirement": "simple==0.0.1 --hash=sha256:deadbeef", 242 }, 243 }) 244 pypi.whl_mods().contains_exactly({}) 245 246_tests.append(_test_simple_multiple_requirements) 247 248def _test_simple_with_markers(env): 249 pypi = _parse_modules( 250 env, 251 module_ctx = _mock_mctx( 252 _mod( 253 name = "rules_python", 254 parse = [ 255 _parse( 256 hub_name = "pypi", 257 python_version = "3.15", 258 requirements_lock = "universal.txt", 259 ), 260 ], 261 ), 262 read = lambda x: { 263 "universal.txt": """\ 264torch==2.4.1+cpu ; platform_machine == 'x86_64' 265torch==2.4.1 ; platform_machine != 'x86_64' 266""", 267 }[x], 268 ), 269 available_interpreters = { 270 "python_3_15_host": "unit_test_interpreter_target", 271 }, 272 evaluate_markers = lambda _, requirements, **__: { 273 key: [ 274 platform 275 for platform in platforms 276 if ("x86_64" in platform and "platform_machine ==" in key) or ("x86_64" not in platform and "platform_machine !=" in key) 277 ] 278 for key, platforms in requirements.items() 279 }, 280 ) 281 282 pypi.is_reproducible().equals(True) 283 pypi.exposed_packages().contains_exactly({"pypi": ["torch"]}) 284 pypi.hub_group_map().contains_exactly({"pypi": {}}) 285 pypi.hub_whl_map().contains_exactly({"pypi": { 286 "torch": { 287 "pypi_315_torch_linux_aarch64_linux_arm_linux_ppc_linux_s390x_osx_aarch64": [ 288 whl_config_setting( 289 target_platforms = [ 290 "cp315_linux_aarch64", 291 "cp315_linux_arm", 292 "cp315_linux_ppc", 293 "cp315_linux_s390x", 294 "cp315_osx_aarch64", 295 ], 296 version = "3.15", 297 ), 298 ], 299 "pypi_315_torch_linux_x86_64_osx_x86_64_windows_x86_64": [ 300 whl_config_setting( 301 target_platforms = [ 302 "cp315_linux_x86_64", 303 "cp315_osx_x86_64", 304 "cp315_windows_x86_64", 305 ], 306 version = "3.15", 307 ), 308 ], 309 }, 310 }}) 311 pypi.whl_libraries().contains_exactly({ 312 "pypi_315_torch_linux_aarch64_linux_arm_linux_ppc_linux_s390x_osx_aarch64": { 313 "dep_template": "@pypi//{name}:{target}", 314 "python_interpreter_target": "unit_test_interpreter_target", 315 "repo": "pypi_315", 316 "requirement": "torch==2.4.1 ; platform_machine != 'x86_64'", 317 }, 318 "pypi_315_torch_linux_x86_64_osx_x86_64_windows_x86_64": { 319 "dep_template": "@pypi//{name}:{target}", 320 "python_interpreter_target": "unit_test_interpreter_target", 321 "repo": "pypi_315", 322 "requirement": "torch==2.4.1+cpu ; platform_machine == 'x86_64'", 323 }, 324 }) 325 pypi.whl_mods().contains_exactly({}) 326 327_tests.append(_test_simple_with_markers) 328 329def _test_download_only_multiple(env): 330 pypi = _parse_modules( 331 env, 332 module_ctx = _mock_mctx( 333 _mod( 334 name = "rules_python", 335 parse = [ 336 _parse( 337 hub_name = "pypi", 338 python_version = "3.15", 339 download_only = True, 340 requirements_by_platform = { 341 "requirements.linux_x86_64.txt": "linux_x86_64", 342 "requirements.osx_aarch64.txt": "osx_aarch64", 343 }, 344 ), 345 ], 346 ), 347 read = lambda x: { 348 "requirements.linux_x86_64.txt": """\ 349--platform=manylinux_2_17_x86_64 350--python-version=315 351--implementation=cp 352--abi=cp315 353 354simple==0.0.1 --hash=sha256:deadbeef 355extra==0.0.1 --hash=sha256:deadb00f 356""", 357 "requirements.osx_aarch64.txt": """\ 358--platform=macosx_10_9_arm64 359--python-version=315 360--implementation=cp 361--abi=cp315 362 363simple==0.0.3 --hash=sha256:deadbaaf 364""", 365 }[x], 366 ), 367 available_interpreters = { 368 "python_3_15_host": "unit_test_interpreter_target", 369 }, 370 ) 371 372 pypi.is_reproducible().equals(True) 373 pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) 374 pypi.hub_group_map().contains_exactly({"pypi": {}}) 375 pypi.hub_whl_map().contains_exactly({"pypi": { 376 "extra": { 377 "pypi_315_extra": [ 378 whl_config_setting(version = "3.15"), 379 ], 380 }, 381 "simple": { 382 "pypi_315_simple_linux_x86_64": [ 383 whl_config_setting( 384 target_platforms = ["cp315_linux_x86_64"], 385 version = "3.15", 386 ), 387 ], 388 "pypi_315_simple_osx_aarch64": [ 389 whl_config_setting( 390 target_platforms = ["cp315_osx_aarch64"], 391 version = "3.15", 392 ), 393 ], 394 }, 395 }}) 396 pypi.whl_libraries().contains_exactly({ 397 "pypi_315_extra": { 398 "dep_template": "@pypi//{name}:{target}", 399 "download_only": True, 400 "experimental_target_platforms": ["cp315_linux_x86_64"], 401 "extra_pip_args": ["--platform=manylinux_2_17_x86_64", "--python-version=315", "--implementation=cp", "--abi=cp315"], 402 "python_interpreter_target": "unit_test_interpreter_target", 403 "repo": "pypi_315", 404 "requirement": "extra==0.0.1 --hash=sha256:deadb00f", 405 }, 406 "pypi_315_simple_linux_x86_64": { 407 "dep_template": "@pypi//{name}:{target}", 408 "download_only": True, 409 "experimental_target_platforms": ["cp315_linux_x86_64"], 410 "extra_pip_args": ["--platform=manylinux_2_17_x86_64", "--python-version=315", "--implementation=cp", "--abi=cp315"], 411 "python_interpreter_target": "unit_test_interpreter_target", 412 "repo": "pypi_315", 413 "requirement": "simple==0.0.1 --hash=sha256:deadbeef", 414 }, 415 "pypi_315_simple_osx_aarch64": { 416 "dep_template": "@pypi//{name}:{target}", 417 "download_only": True, 418 "experimental_target_platforms": ["cp315_osx_aarch64"], 419 "extra_pip_args": ["--platform=macosx_10_9_arm64", "--python-version=315", "--implementation=cp", "--abi=cp315"], 420 "python_interpreter_target": "unit_test_interpreter_target", 421 "repo": "pypi_315", 422 "requirement": "simple==0.0.3 --hash=sha256:deadbaaf", 423 }, 424 }) 425 pypi.whl_mods().contains_exactly({}) 426 427_tests.append(_test_download_only_multiple) 428 429def _test_simple_get_index(env): 430 got_simpleapi_download_args = [] 431 got_simpleapi_download_kwargs = {} 432 433 def mocksimpleapi_download(*args, **kwargs): 434 got_simpleapi_download_args.extend(args) 435 got_simpleapi_download_kwargs.update(kwargs) 436 return { 437 "simple": struct( 438 whls = { 439 "deadb00f": struct( 440 yanked = False, 441 filename = "simple-0.0.1-py3-none-any.whl", 442 sha256 = "deadb00f", 443 url = "example2.org", 444 ), 445 }, 446 sdists = { 447 "deadbeef": struct( 448 yanked = False, 449 filename = "simple-0.0.1.tar.gz", 450 sha256 = "deadbeef", 451 url = "example.org", 452 ), 453 }, 454 ), 455 } 456 457 pypi = _parse_modules( 458 env, 459 module_ctx = _mock_mctx( 460 _mod( 461 name = "rules_python", 462 parse = [ 463 _parse( 464 hub_name = "pypi", 465 python_version = "3.15", 466 requirements_lock = "requirements.txt", 467 experimental_index_url = "pypi.org", 468 extra_pip_args = [ 469 "--extra-args-for-sdist-building", 470 ], 471 ), 472 ], 473 ), 474 read = lambda x: { 475 "requirements.txt": """ 476simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadb00f 477some_pkg==0.0.1 478""", 479 }[x], 480 ), 481 available_interpreters = { 482 "python_3_15_host": "unit_test_interpreter_target", 483 }, 484 simpleapi_download = mocksimpleapi_download, 485 ) 486 487 pypi.is_reproducible().equals(False) 488 pypi.exposed_packages().contains_exactly({"pypi": ["simple", "some_pkg"]}) 489 pypi.hub_group_map().contains_exactly({"pypi": {}}) 490 pypi.hub_whl_map().contains_exactly({ 491 "pypi": { 492 "simple": { 493 "pypi_315_simple_py3_none_any_deadb00f": [ 494 whl_config_setting( 495 filename = "simple-0.0.1-py3-none-any.whl", 496 version = "3.15", 497 ), 498 ], 499 "pypi_315_simple_sdist_deadbeef": [ 500 whl_config_setting( 501 filename = "simple-0.0.1.tar.gz", 502 version = "3.15", 503 ), 504 ], 505 }, 506 "some_pkg": { 507 "pypi_315_some_pkg": [whl_config_setting(version = "3.15")], 508 }, 509 }, 510 }) 511 pypi.whl_libraries().contains_exactly({ 512 "pypi_315_simple_py3_none_any_deadb00f": { 513 "dep_template": "@pypi//{name}:{target}", 514 "experimental_target_platforms": [ 515 "cp315_linux_aarch64", 516 "cp315_linux_arm", 517 "cp315_linux_ppc", 518 "cp315_linux_s390x", 519 "cp315_linux_x86_64", 520 "cp315_osx_aarch64", 521 "cp315_osx_x86_64", 522 "cp315_windows_x86_64", 523 ], 524 "filename": "simple-0.0.1-py3-none-any.whl", 525 "python_interpreter_target": "unit_test_interpreter_target", 526 "repo": "pypi_315", 527 "requirement": "simple==0.0.1", 528 "sha256": "deadb00f", 529 "urls": ["example2.org"], 530 }, 531 "pypi_315_simple_sdist_deadbeef": { 532 "dep_template": "@pypi//{name}:{target}", 533 "experimental_target_platforms": [ 534 "cp315_linux_aarch64", 535 "cp315_linux_arm", 536 "cp315_linux_ppc", 537 "cp315_linux_s390x", 538 "cp315_linux_x86_64", 539 "cp315_osx_aarch64", 540 "cp315_osx_x86_64", 541 "cp315_windows_x86_64", 542 ], 543 "extra_pip_args": ["--extra-args-for-sdist-building"], 544 "filename": "simple-0.0.1.tar.gz", 545 "python_interpreter_target": "unit_test_interpreter_target", 546 "repo": "pypi_315", 547 "requirement": "simple==0.0.1", 548 "sha256": "deadbeef", 549 "urls": ["example.org"], 550 }, 551 # We are falling back to regular `pip` 552 "pypi_315_some_pkg": { 553 "dep_template": "@pypi//{name}:{target}", 554 "extra_pip_args": ["--extra-args-for-sdist-building"], 555 "python_interpreter_target": "unit_test_interpreter_target", 556 "repo": "pypi_315", 557 "requirement": "some_pkg==0.0.1", 558 }, 559 }) 560 pypi.whl_mods().contains_exactly({}) 561 env.expect.that_dict(got_simpleapi_download_kwargs).contains_exactly({ 562 "attr": struct( 563 auth_patterns = {}, 564 envsubst = {}, 565 extra_index_urls = [], 566 index_url = "pypi.org", 567 index_url_overrides = {}, 568 netrc = None, 569 sources = ["simple", "some_pkg"], 570 ), 571 "cache": {}, 572 "parallel_download": False, 573 }) 574 575_tests.append(_test_simple_get_index) 576 577def extension_test_suite(name): 578 """Create the test suite. 579 580 Args: 581 name: the name of the test suite 582 """ 583 test_suite(name = name, basic_tests = _tests) 584