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"""Tests for construction of Python version matching config settings.""" 15 16load("@rules_testing//lib:analysis_test.bzl", "analysis_test") 17load("@rules_testing//lib:test_suite.bzl", "test_suite") 18load("@rules_testing//lib:truth.bzl", "subjects") 19load("@rules_testing//lib:util.bzl", test_util = "util") 20load("//python/private/pypi:config_settings.bzl", "config_settings") # buildifier: disable=bzl-visibility 21 22def _subject_impl(ctx): 23 _ = ctx # @unused 24 return [DefaultInfo()] 25 26_subject = rule( 27 implementation = _subject_impl, 28 attrs = { 29 "dist": attr.string(), 30 }, 31) 32 33_flag = struct( 34 platform = lambda x: ("//command_line_option:platforms", str(Label("//tests/support:" + x))), 35 pip_whl = lambda x: (str(Label("//python/config_settings:pip_whl")), str(x)), 36 pip_whl_glibc_version = lambda x: (str(Label("//python/config_settings:pip_whl_glibc_version")), str(x)), 37 pip_whl_muslc_version = lambda x: (str(Label("//python/config_settings:pip_whl_muslc_version")), str(x)), 38 pip_whl_osx_version = lambda x: (str(Label("//python/config_settings:pip_whl_osx_version")), str(x)), 39 pip_whl_osx_arch = lambda x: (str(Label("//python/config_settings:pip_whl_osx_arch")), str(x)), 40 py_linux_libc = lambda x: (str(Label("//python/config_settings:py_linux_libc")), str(x)), 41 python_version = lambda x: (str(Label("//python/config_settings:python_version")), str(x)), 42) 43 44def _analysis_test(*, name, dist, want, config_settings = [_flag.platform("linux_aarch64")]): 45 subject_name = name + "_subject" 46 test_util.helper_target( 47 _subject, 48 name = subject_name, 49 dist = select( 50 dist | { 51 "//conditions:default": "no_match", 52 }, 53 ), 54 ) 55 config_settings = dict(config_settings) 56 if not config_settings: 57 fail("For reproducibility on different platforms, the config setting must be specified") 58 python_version, default_value = _flag.python_version("3.7.10") 59 config_settings.setdefault(python_version, default_value) 60 61 analysis_test( 62 name = name, 63 target = subject_name, 64 impl = lambda env, target: _match(env, target, want), 65 config_settings = config_settings, 66 ) 67 68def _match(env, target, want): 69 target = env.expect.that_target(target) 70 target.attr("dist", factory = subjects.str).equals(want) 71 72_tests = [] 73 74# Tests when we only have an `sdist` present. 75 76def _test_sdist_default(name): 77 _analysis_test( 78 name = name, 79 dist = { 80 "is_cp3.7_sdist": "sdist", 81 }, 82 want = "sdist", 83 ) 84 85_tests.append(_test_sdist_default) 86 87def _test_sdist_no_whl(name): 88 _analysis_test( 89 name = name, 90 dist = { 91 "is_cp3.7_sdist": "sdist", 92 }, 93 config_settings = [ 94 _flag.platform("linux_aarch64"), 95 _flag.pip_whl("no"), 96 ], 97 want = "sdist", 98 ) 99 100_tests.append(_test_sdist_no_whl) 101 102def _test_sdist_no_sdist(name): 103 _analysis_test( 104 name = name, 105 dist = { 106 "is_cp3.7_sdist": "sdist", 107 }, 108 config_settings = [ 109 _flag.platform("linux_aarch64"), 110 _flag.pip_whl("only"), 111 ], 112 # We will use `no_match_error` in the real case to indicate that `sdist` is not 113 # allowed to be used. 114 want = "no_match", 115 ) 116 117_tests.append(_test_sdist_no_sdist) 118 119def _test_basic_whl_default(name): 120 _analysis_test( 121 name = name, 122 dist = { 123 "is_cp3.7_py_none_any": "whl", 124 "is_cp3.7_sdist": "sdist", 125 }, 126 want = "whl", 127 ) 128 129_tests.append(_test_basic_whl_default) 130 131def _test_basic_whl_nowhl(name): 132 _analysis_test( 133 name = name, 134 dist = { 135 "is_cp3.7_py_none_any": "whl", 136 "is_cp3.7_sdist": "sdist", 137 }, 138 config_settings = [ 139 _flag.platform("linux_aarch64"), 140 _flag.pip_whl("no"), 141 ], 142 want = "sdist", 143 ) 144 145_tests.append(_test_basic_whl_nowhl) 146 147def _test_basic_whl_nosdist(name): 148 _analysis_test( 149 name = name, 150 dist = { 151 "is_cp3.7_py_none_any": "whl", 152 "is_cp3.7_sdist": "sdist", 153 }, 154 config_settings = [ 155 _flag.platform("linux_aarch64"), 156 _flag.pip_whl("only"), 157 ], 158 want = "whl", 159 ) 160 161_tests.append(_test_basic_whl_nosdist) 162 163def _test_whl_default(name): 164 _analysis_test( 165 name = name, 166 dist = { 167 "is_cp3.7_py3_none_any": "whl", 168 "is_cp3.7_py_none_any": "basic_whl", 169 }, 170 want = "whl", 171 ) 172 173_tests.append(_test_whl_default) 174 175def _test_whl_nowhl(name): 176 _analysis_test( 177 name = name, 178 dist = { 179 "is_cp3.7_py3_none_any": "whl", 180 "is_cp3.7_py_none_any": "basic_whl", 181 }, 182 config_settings = [ 183 _flag.platform("linux_aarch64"), 184 _flag.pip_whl("no"), 185 ], 186 want = "no_match", 187 ) 188 189_tests.append(_test_whl_nowhl) 190 191def _test_whl_nosdist(name): 192 _analysis_test( 193 name = name, 194 dist = { 195 "is_cp3.7_py3_none_any": "whl", 196 }, 197 config_settings = [ 198 _flag.platform("linux_aarch64"), 199 _flag.pip_whl("only"), 200 ], 201 want = "whl", 202 ) 203 204_tests.append(_test_whl_nosdist) 205 206def _test_abi_whl_is_prefered(name): 207 _analysis_test( 208 name = name, 209 dist = { 210 "is_cp3.7_py3_abi3_any": "abi_whl", 211 "is_cp3.7_py3_none_any": "whl", 212 }, 213 want = "abi_whl", 214 ) 215 216_tests.append(_test_abi_whl_is_prefered) 217 218def _test_whl_with_constraints_is_prefered(name): 219 _analysis_test( 220 name = name, 221 dist = { 222 "is_cp3.7_py3_none_any": "default_whl", 223 "is_cp3.7_py3_none_any_linux_aarch64": "whl", 224 "is_cp3.7_py3_none_any_linux_x86_64": "amd64_whl", 225 }, 226 want = "whl", 227 ) 228 229_tests.append(_test_whl_with_constraints_is_prefered) 230 231def _test_cp_whl_is_prefered_over_py3(name): 232 _analysis_test( 233 name = name, 234 dist = { 235 "is_cp3.7_cp3x_none_any": "cp", 236 "is_cp3.7_py3_abi3_any": "py3_abi3", 237 "is_cp3.7_py3_none_any": "py3", 238 }, 239 want = "cp", 240 ) 241 242_tests.append(_test_cp_whl_is_prefered_over_py3) 243 244def _test_cp_abi_whl_is_prefered_over_py3(name): 245 _analysis_test( 246 name = name, 247 dist = { 248 "is_cp3.7_cp3x_abi3_any": "cp", 249 "is_cp3.7_py3_abi3_any": "py3", 250 }, 251 want = "cp", 252 ) 253 254_tests.append(_test_cp_abi_whl_is_prefered_over_py3) 255 256def _test_cp_version_is_selected_when_python_version_is_specified(name): 257 _analysis_test( 258 name = name, 259 dist = { 260 "is_cp3.10_cp3x_none_any": "cp310", 261 "is_cp3.8_cp3x_none_any": "cp38", 262 "is_cp3.9_cp3x_none_any": "cp39", 263 }, 264 want = "cp310", 265 config_settings = [ 266 _flag.python_version("3.10.9"), 267 _flag.platform("linux_aarch64"), 268 ], 269 ) 270 271_tests.append(_test_cp_version_is_selected_when_python_version_is_specified) 272 273def _test_py_none_any_versioned(name): 274 _analysis_test( 275 name = name, 276 dist = { 277 "is_cp3.10_py_none_any": "whl", 278 "is_cp3.9_py_none_any": "too-low", 279 }, 280 want = "whl", 281 config_settings = [ 282 _flag.python_version("3.10.9"), 283 _flag.platform("linux_aarch64"), 284 ], 285 ) 286 287_tests.append(_test_py_none_any_versioned) 288 289def _test_cp_cp_whl(name): 290 _analysis_test( 291 name = name, 292 dist = { 293 "is_cp3.10_cp3x_cp_linux_aarch64": "whl", 294 }, 295 want = "whl", 296 config_settings = [ 297 _flag.python_version("3.10.9"), 298 _flag.platform("linux_aarch64"), 299 ], 300 ) 301 302_tests.append(_test_cp_cp_whl) 303 304def _test_cp_version_sdist_is_selected(name): 305 _analysis_test( 306 name = name, 307 dist = { 308 "is_cp3.10_sdist": "sdist", 309 }, 310 want = "sdist", 311 config_settings = [ 312 _flag.python_version("3.10.9"), 313 _flag.platform("linux_aarch64"), 314 ], 315 ) 316 317_tests.append(_test_cp_version_sdist_is_selected) 318 319def _test_platform_whl_is_prefered_over_any_whl_with_constraints(name): 320 _analysis_test( 321 name = name, 322 dist = { 323 "is_cp3.7_py3_abi3_any": "better_default_whl", 324 "is_cp3.7_py3_abi3_any_linux_aarch64": "better_default_any_whl", 325 "is_cp3.7_py3_none_any": "default_whl", 326 "is_cp3.7_py3_none_any_linux_aarch64": "whl", 327 "is_cp3.7_py3_none_linux_aarch64": "platform_whl", 328 }, 329 want = "platform_whl", 330 ) 331 332_tests.append(_test_platform_whl_is_prefered_over_any_whl_with_constraints) 333 334def _test_abi3_platform_whl_preference(name): 335 _analysis_test( 336 name = name, 337 dist = { 338 "is_cp3.7_py3_abi3_linux_aarch64": "abi3_platform", 339 "is_cp3.7_py3_none_linux_aarch64": "platform", 340 }, 341 want = "abi3_platform", 342 ) 343 344_tests.append(_test_abi3_platform_whl_preference) 345 346def _test_glibc(name): 347 _analysis_test( 348 name = name, 349 dist = { 350 "is_cp3.7_cp3x_cp_manylinux_aarch64": "glibc", 351 "is_cp3.7_py3_abi3_linux_aarch64": "abi3_platform", 352 }, 353 want = "glibc", 354 ) 355 356_tests.append(_test_glibc) 357 358def _test_glibc_versioned(name): 359 _analysis_test( 360 name = name, 361 dist = { 362 "is_cp3.7_cp3x_cp_manylinux_2_14_aarch64": "glibc", 363 "is_cp3.7_cp3x_cp_manylinux_2_17_aarch64": "glibc", 364 "is_cp3.7_py3_abi3_linux_aarch64": "abi3_platform", 365 }, 366 want = "glibc", 367 config_settings = [ 368 _flag.py_linux_libc("glibc"), 369 _flag.pip_whl_glibc_version("2.17"), 370 _flag.platform("linux_aarch64"), 371 ], 372 ) 373 374_tests.append(_test_glibc_versioned) 375 376def _test_glibc_compatible_exists(name): 377 _analysis_test( 378 name = name, 379 dist = { 380 # Code using the conditions will need to construct selects, which 381 # do the version matching correctly. 382 "is_cp3.7_cp3x_cp_manylinux_2_14_aarch64": "2_14_whl_via_2_14_branch", 383 "is_cp3.7_cp3x_cp_manylinux_2_17_aarch64": "2_14_whl_via_2_17_branch", 384 }, 385 want = "2_14_whl_via_2_17_branch", 386 config_settings = [ 387 _flag.py_linux_libc("glibc"), 388 _flag.pip_whl_glibc_version("2.17"), 389 _flag.platform("linux_aarch64"), 390 ], 391 ) 392 393_tests.append(_test_glibc_compatible_exists) 394 395def _test_musl(name): 396 _analysis_test( 397 name = name, 398 dist = { 399 "is_cp3.7_cp3x_cp_musllinux_aarch64": "musl", 400 }, 401 want = "musl", 402 config_settings = [ 403 _flag.py_linux_libc("musl"), 404 _flag.platform("linux_aarch64"), 405 ], 406 ) 407 408_tests.append(_test_musl) 409 410def _test_windows(name): 411 _analysis_test( 412 name = name, 413 dist = { 414 "is_cp3.7_cp3x_cp_windows_x86_64": "whl", 415 }, 416 want = "whl", 417 config_settings = [ 418 _flag.platform("windows_x86_64"), 419 ], 420 ) 421 422_tests.append(_test_windows) 423 424def _test_osx(name): 425 _analysis_test( 426 name = name, 427 dist = { 428 # We prefer arch specific whls over universal 429 "is_cp3.7_cp3x_cp_osx_x86_64": "whl", 430 "is_cp3.7_cp3x_cp_osx_x86_64_universal2": "universal_whl", 431 }, 432 want = "whl", 433 config_settings = [ 434 _flag.platform("mac_x86_64"), 435 ], 436 ) 437 438_tests.append(_test_osx) 439 440def _test_osx_universal_default(name): 441 _analysis_test( 442 name = name, 443 dist = { 444 # We default to universal if only that exists 445 "is_cp3.7_cp3x_cp_osx_x86_64_universal2": "whl", 446 }, 447 want = "whl", 448 config_settings = [ 449 _flag.platform("mac_x86_64"), 450 ], 451 ) 452 453_tests.append(_test_osx_universal_default) 454 455def _test_osx_universal_only(name): 456 _analysis_test( 457 name = name, 458 dist = { 459 # If we prefer universal, then we use that 460 "is_cp3.7_cp3x_cp_osx_x86_64": "whl", 461 "is_cp3.7_cp3x_cp_osx_x86_64_universal2": "universal", 462 }, 463 want = "universal", 464 config_settings = [ 465 _flag.pip_whl_osx_arch("universal"), 466 _flag.platform("mac_x86_64"), 467 ], 468 ) 469 470_tests.append(_test_osx_universal_only) 471 472def _test_osx_os_version(name): 473 _analysis_test( 474 name = name, 475 dist = { 476 # Similarly to the libc version, the user of the config settings will have to 477 # construct the select so that the version selection is correct. 478 "is_cp3.7_cp3x_cp_osx_10_9_x86_64": "whl", 479 }, 480 want = "whl", 481 config_settings = [ 482 _flag.pip_whl_osx_version("10.9"), 483 _flag.platform("mac_x86_64"), 484 ], 485 ) 486 487_tests.append(_test_osx_os_version) 488 489def _test_all(name): 490 _analysis_test( 491 name = name, 492 dist = { 493 "is_cp3.7_" + f: f 494 for f in [ 495 "{py}_{abi}_{plat}".format(py = valid_py, abi = valid_abi, plat = valid_plat) 496 # we have py2.py3, py3, cp3x 497 for valid_py in ["py", "py3", "cp3x"] 498 # cp abi usually comes with a version and we only need one 499 # config setting variant for all of them because the python 500 # version will discriminate between different versions. 501 for valid_abi in ["none", "abi3", "cp"] 502 for valid_plat in [ 503 "any", 504 "manylinux_2_17_x86_64", 505 "manylinux_2_17_aarch64", 506 "osx_x86_64", 507 "windows_x86_64", 508 ] 509 if not ( 510 valid_abi == "abi3" and valid_py == "py" or 511 valid_abi == "cp" and valid_py != "cp3x" 512 ) 513 ] 514 }, 515 want = "cp3x_cp_manylinux_2_17_x86_64", 516 config_settings = [ 517 _flag.pip_whl_glibc_version("2.17"), 518 _flag.platform("linux_x86_64"), 519 ], 520 ) 521 522_tests.append(_test_all) 523 524def config_settings_test_suite(name): # buildifier: disable=function-docstring 525 test_suite( 526 name = name, 527 tests = _tests, 528 ) 529 530 config_settings( 531 name = "dummy", 532 python_versions = ["3.7", "3.8", "3.9", "3.10"], 533 glibc_versions = [(2, 14), (2, 17)], 534 muslc_versions = [(1, 1)], 535 osx_versions = [(10, 9), (11, 0)], 536 target_platforms = [ 537 "windows_x86_64", 538 "windows_aarch64", 539 "linux_x86_64", 540 "linux_ppc", 541 "linux_aarch64", 542 "osx_x86_64", 543 "osx_aarch64", 544 ], 545 ) 546