• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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