xref: /aosp_15_r20/external/bazelbuild-rules_python/tests/python/python_tests.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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