xref: /aosp_15_r20/external/bazelbuild-rules_python/tests/py_runtime/py_runtime_tests.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
1# Copyright 2023 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"""Starlark tests for py_runtime rule."""
15
16load("@rules_python_internal//:rules_python_config.bzl", "config")
17load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
18load("@rules_testing//lib:test_suite.bzl", "test_suite")
19load("@rules_testing//lib:truth.bzl", "matching")
20load("@rules_testing//lib:util.bzl", rt_util = "util")
21load("//python:py_runtime.bzl", "py_runtime")
22load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
23load("//tests/base_rules:util.bzl", br_util = "util")
24load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject")
25load("//tests/support:support.bzl", "PYTHON_VERSION")
26
27_tests = []
28
29_SKIP_TEST = {
30    "target_compatible_with": ["@platforms//:incompatible"],
31}
32
33def _simple_binary_impl(ctx):
34    executable = ctx.actions.declare_file(ctx.label.name)
35    ctx.actions.write(executable, "", is_executable = True)
36    return [DefaultInfo(
37        executable = executable,
38        files = depset([executable] + ctx.files.extra_default_outputs),
39        runfiles = ctx.runfiles(ctx.files.data),
40    )]
41
42_simple_binary = rule(
43    implementation = _simple_binary_impl,
44    attrs = {
45        "data": attr.label_list(allow_files = True),
46        "extra_default_outputs": attr.label_list(allow_files = True),
47    },
48    executable = True,
49)
50
51def _test_bootstrap_template(name):
52    # The bootstrap_template arg isn't present in older Bazel versions, so
53    # we have to conditionally pass the arg and mark the test incompatible.
54    if config.enable_pystar:
55        py_runtime_kwargs = {"bootstrap_template": "bootstrap.txt"}
56        attr_values = {}
57    else:
58        py_runtime_kwargs = {}
59        attr_values = _SKIP_TEST
60
61    rt_util.helper_target(
62        py_runtime,
63        name = name + "_subject",
64        interpreter_path = "/py",
65        python_version = "PY3",
66        **py_runtime_kwargs
67    )
68    analysis_test(
69        name = name,
70        target = name + "_subject",
71        impl = _test_bootstrap_template_impl,
72        attr_values = attr_values,
73    )
74
75def _test_bootstrap_template_impl(env, target):
76    env.expect.that_target(target).provider(
77        PyRuntimeInfo,
78        factory = py_runtime_info_subject,
79    ).bootstrap_template().path().contains("bootstrap.txt")
80
81_tests.append(_test_bootstrap_template)
82
83def _test_cannot_have_both_inbuild_and_system_interpreter(name):
84    if br_util.is_bazel_6_or_higher():
85        py_runtime_kwargs = {
86            "interpreter": "fake_interpreter",
87            "interpreter_path": "/some/path",
88        }
89        attr_values = {}
90    else:
91        py_runtime_kwargs = {
92            "interpreter_path": "/some/path",
93        }
94        attr_values = _SKIP_TEST
95    rt_util.helper_target(
96        py_runtime,
97        name = name + "_subject",
98        python_version = "PY3",
99        **py_runtime_kwargs
100    )
101    analysis_test(
102        name = name,
103        target = name + "_subject",
104        impl = _test_cannot_have_both_inbuild_and_system_interpreter_impl,
105        expect_failure = True,
106        attr_values = attr_values,
107    )
108
109def _test_cannot_have_both_inbuild_and_system_interpreter_impl(env, target):
110    env.expect.that_target(target).failures().contains_predicate(
111        matching.str_matches("one of*interpreter*interpreter_path"),
112    )
113
114_tests.append(_test_cannot_have_both_inbuild_and_system_interpreter)
115
116def _test_cannot_specify_files_for_system_interpreter(name):
117    if br_util.is_bazel_6_or_higher():
118        py_runtime_kwargs = {"files": ["foo.txt"]}
119        attr_values = {}
120    else:
121        py_runtime_kwargs = {}
122        attr_values = _SKIP_TEST
123    rt_util.helper_target(
124        py_runtime,
125        name = name + "_subject",
126        interpreter_path = "/foo",
127        python_version = "PY3",
128        **py_runtime_kwargs
129    )
130    analysis_test(
131        name = name,
132        target = name + "_subject",
133        impl = _test_cannot_specify_files_for_system_interpreter_impl,
134        expect_failure = True,
135        attr_values = attr_values,
136    )
137
138def _test_cannot_specify_files_for_system_interpreter_impl(env, target):
139    env.expect.that_target(target).failures().contains_predicate(
140        matching.str_matches("files*must be empty"),
141    )
142
143_tests.append(_test_cannot_specify_files_for_system_interpreter)
144
145def _test_coverage_tool_executable(name):
146    if br_util.is_bazel_6_or_higher():
147        py_runtime_kwargs = {
148            "coverage_tool": name + "_coverage_tool",
149        }
150        attr_values = {}
151    else:
152        py_runtime_kwargs = {}
153        attr_values = _SKIP_TEST
154
155    rt_util.helper_target(
156        py_runtime,
157        name = name + "_subject",
158        python_version = "PY3",
159        interpreter_path = "/bogus",
160        **py_runtime_kwargs
161    )
162    rt_util.helper_target(
163        _simple_binary,
164        name = name + "_coverage_tool",
165        data = ["coverage_file1.txt", "coverage_file2.txt"],
166    )
167    analysis_test(
168        name = name,
169        target = name + "_subject",
170        impl = _test_coverage_tool_executable_impl,
171        attr_values = attr_values,
172    )
173
174def _test_coverage_tool_executable_impl(env, target):
175    info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject)
176    info.coverage_tool().short_path_equals("{package}/{test_name}_coverage_tool")
177    info.coverage_files().contains_exactly([
178        "{package}/{test_name}_coverage_tool",
179        "{package}/coverage_file1.txt",
180        "{package}/coverage_file2.txt",
181    ])
182
183_tests.append(_test_coverage_tool_executable)
184
185def _test_coverage_tool_plain_files(name):
186    if br_util.is_bazel_6_or_higher():
187        py_runtime_kwargs = {
188            "coverage_tool": name + "_coverage_tool",
189        }
190        attr_values = {}
191    else:
192        py_runtime_kwargs = {}
193        attr_values = _SKIP_TEST
194    rt_util.helper_target(
195        py_runtime,
196        name = name + "_subject",
197        python_version = "PY3",
198        interpreter_path = "/bogus",
199        **py_runtime_kwargs
200    )
201    rt_util.helper_target(
202        native.filegroup,
203        name = name + "_coverage_tool",
204        srcs = ["coverage_tool.py"],
205        data = ["coverage_file1.txt", "coverage_file2.txt"],
206    )
207    analysis_test(
208        name = name,
209        target = name + "_subject",
210        impl = _test_coverage_tool_plain_files_impl,
211        attr_values = attr_values,
212    )
213
214def _test_coverage_tool_plain_files_impl(env, target):
215    info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject)
216    info.coverage_tool().short_path_equals("{package}/coverage_tool.py")
217    info.coverage_files().contains_exactly([
218        "{package}/coverage_tool.py",
219        "{package}/coverage_file1.txt",
220        "{package}/coverage_file2.txt",
221    ])
222
223_tests.append(_test_coverage_tool_plain_files)
224
225def _test_in_build_interpreter(name):
226    rt_util.helper_target(
227        py_runtime,
228        name = name + "_subject",
229        interpreter = "fake_interpreter",
230        python_version = "PY3",
231        files = ["file1.txt"],
232    )
233    analysis_test(
234        name = name,
235        target = name + "_subject",
236        impl = _test_in_build_interpreter_impl,
237    )
238
239def _test_in_build_interpreter_impl(env, target):
240    info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject)
241    info.python_version().equals("PY3")
242    info.files().contains_predicate(matching.file_basename_equals("file1.txt"))
243    info.interpreter().path().contains("fake_interpreter")
244
245_tests.append(_test_in_build_interpreter)
246
247def _test_interpreter_binary_with_multiple_outputs(name):
248    rt_util.helper_target(
249        _simple_binary,
250        name = name + "_built_interpreter",
251        extra_default_outputs = ["extra_default_output.txt"],
252        data = ["runfile.txt"],
253    )
254
255    rt_util.helper_target(
256        py_runtime,
257        name = name + "_subject",
258        interpreter = name + "_built_interpreter",
259        python_version = "PY3",
260    )
261    analysis_test(
262        name = name,
263        target = name + "_subject",
264        impl = _test_interpreter_binary_with_multiple_outputs_impl,
265    )
266
267def _test_interpreter_binary_with_multiple_outputs_impl(env, target):
268    target = env.expect.that_target(target)
269    py_runtime_info = target.provider(
270        PyRuntimeInfo,
271        factory = py_runtime_info_subject,
272    )
273    py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter")
274    py_runtime_info.files().contains_exactly([
275        "{package}/extra_default_output.txt",
276        "{package}/runfile.txt",
277        "{package}/{test_name}_built_interpreter",
278    ])
279
280    target.default_outputs().contains_exactly([
281        "{package}/extra_default_output.txt",
282        "{package}/runfile.txt",
283        "{package}/{test_name}_built_interpreter",
284    ])
285
286    target.runfiles().contains_exactly([
287        "{workspace}/{package}/runfile.txt",
288        "{workspace}/{package}/{test_name}_built_interpreter",
289    ])
290
291_tests.append(_test_interpreter_binary_with_multiple_outputs)
292
293def _test_interpreter_binary_with_single_output_and_runfiles(name):
294    rt_util.helper_target(
295        _simple_binary,
296        name = name + "_built_interpreter",
297        data = ["runfile.txt"],
298    )
299
300    rt_util.helper_target(
301        py_runtime,
302        name = name + "_subject",
303        interpreter = name + "_built_interpreter",
304        python_version = "PY3",
305    )
306    analysis_test(
307        name = name,
308        target = name + "_subject",
309        impl = _test_interpreter_binary_with_single_output_and_runfiles_impl,
310    )
311
312def _test_interpreter_binary_with_single_output_and_runfiles_impl(env, target):
313    target = env.expect.that_target(target)
314    py_runtime_info = target.provider(
315        PyRuntimeInfo,
316        factory = py_runtime_info_subject,
317    )
318    py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter")
319    py_runtime_info.files().contains_exactly([
320        "{package}/runfile.txt",
321        "{package}/{test_name}_built_interpreter",
322    ])
323
324    target.default_outputs().contains_exactly([
325        "{package}/runfile.txt",
326        "{package}/{test_name}_built_interpreter",
327    ])
328
329    target.runfiles().contains_exactly([
330        "{workspace}/{package}/runfile.txt",
331        "{workspace}/{package}/{test_name}_built_interpreter",
332    ])
333
334_tests.append(_test_interpreter_binary_with_single_output_and_runfiles)
335
336def _test_must_have_either_inbuild_or_system_interpreter(name):
337    if br_util.is_bazel_6_or_higher():
338        py_runtime_kwargs = {}
339        attr_values = {}
340    else:
341        py_runtime_kwargs = {
342            "interpreter_path": "/some/path",
343        }
344        attr_values = _SKIP_TEST
345    rt_util.helper_target(
346        py_runtime,
347        name = name + "_subject",
348        python_version = "PY3",
349        **py_runtime_kwargs
350    )
351    analysis_test(
352        name = name,
353        target = name + "_subject",
354        impl = _test_must_have_either_inbuild_or_system_interpreter_impl,
355        expect_failure = True,
356        attr_values = attr_values,
357    )
358
359def _test_must_have_either_inbuild_or_system_interpreter_impl(env, target):
360    env.expect.that_target(target).failures().contains_predicate(
361        matching.str_matches("one of*interpreter*interpreter_path"),
362    )
363
364_tests.append(_test_must_have_either_inbuild_or_system_interpreter)
365
366def _test_system_interpreter(name):
367    rt_util.helper_target(
368        py_runtime,
369        name = name + "_subject",
370        interpreter_path = "/system/python",
371        python_version = "PY3",
372    )
373    analysis_test(
374        name = name,
375        target = name + "_subject",
376        impl = _test_system_interpreter_impl,
377    )
378
379def _test_system_interpreter_impl(env, target):
380    env.expect.that_target(target).provider(
381        PyRuntimeInfo,
382        factory = py_runtime_info_subject,
383    ).interpreter_path().equals("/system/python")
384
385_tests.append(_test_system_interpreter)
386
387def _test_system_interpreter_must_be_absolute(name):
388    # Bazel 5.4 will entirely crash when an invalid interpreter_path
389    # is given.
390    if br_util.is_bazel_6_or_higher():
391        py_runtime_kwargs = {"interpreter_path": "relative/path"}
392        attr_values = {}
393    else:
394        py_runtime_kwargs = {"interpreter_path": "/junk/value/for/bazel5.4"}
395        attr_values = _SKIP_TEST
396    rt_util.helper_target(
397        py_runtime,
398        name = name + "_subject",
399        python_version = "PY3",
400        **py_runtime_kwargs
401    )
402    analysis_test(
403        name = name,
404        target = name + "_subject",
405        impl = _test_system_interpreter_must_be_absolute_impl,
406        expect_failure = True,
407        attr_values = attr_values,
408    )
409
410def _test_system_interpreter_must_be_absolute_impl(env, target):
411    env.expect.that_target(target).failures().contains_predicate(
412        matching.str_matches("must be*absolute"),
413    )
414
415_tests.append(_test_system_interpreter_must_be_absolute)
416
417def _interpreter_version_info_test(name, interpreter_version_info, impl, expect_failure = True):
418    if config.enable_pystar:
419        py_runtime_kwargs = {
420            "interpreter_version_info": interpreter_version_info,
421        }
422        attr_values = {}
423    else:
424        py_runtime_kwargs = {}
425        attr_values = _SKIP_TEST
426
427    rt_util.helper_target(
428        py_runtime,
429        name = name + "_subject",
430        python_version = "PY3",
431        interpreter_path = "/py",
432        **py_runtime_kwargs
433    )
434    analysis_test(
435        name = name,
436        target = name + "_subject",
437        impl = impl,
438        expect_failure = expect_failure,
439        attr_values = attr_values,
440    )
441
442def _test_interpreter_version_info_must_define_major_and_minor_only_major(name):
443    _interpreter_version_info_test(
444        name,
445        {
446            "major": "3",
447        },
448        lambda env, target: (
449            env.expect.that_target(target).failures().contains_predicate(
450                matching.str_matches("must have at least two keys, 'major' and 'minor'"),
451            )
452        ),
453    )
454
455_tests.append(_test_interpreter_version_info_must_define_major_and_minor_only_major)
456
457def _test_interpreter_version_info_must_define_major_and_minor_only_minor(name):
458    _interpreter_version_info_test(
459        name,
460        {
461            "minor": "3",
462        },
463        lambda env, target: (
464            env.expect.that_target(target).failures().contains_predicate(
465                matching.str_matches("must have at least two keys, 'major' and 'minor'"),
466            )
467        ),
468    )
469
470_tests.append(_test_interpreter_version_info_must_define_major_and_minor_only_minor)
471
472def _test_interpreter_version_info_no_extraneous_keys(name):
473    _interpreter_version_info_test(
474        name,
475        {
476            "major": "3",
477            "minor": "3",
478            "something": "foo",
479        },
480        lambda env, target: (
481            env.expect.that_target(target).failures().contains_predicate(
482                matching.str_matches("unexpected keys [\"something\"]"),
483            )
484        ),
485    )
486
487_tests.append(_test_interpreter_version_info_no_extraneous_keys)
488
489def _test_interpreter_version_info_sets_values_to_none_if_not_given(name):
490    _interpreter_version_info_test(
491        name,
492        {
493            "major": "3",
494            "micro": "10",
495            "minor": "3",
496        },
497        lambda env, target: (
498            env.expect.that_target(target).provider(
499                PyRuntimeInfo,
500                factory = py_runtime_info_subject,
501            ).interpreter_version_info().serial().equals(None)
502        ),
503        expect_failure = False,
504    )
505
506_tests.append(_test_interpreter_version_info_sets_values_to_none_if_not_given)
507
508def _test_interpreter_version_info_parses_values_to_struct(name):
509    _interpreter_version_info_test(
510        name,
511        {
512            "major": "3",
513            "micro": "10",
514            "minor": "6",
515            "releaselevel": "alpha",
516            "serial": "1",
517        },
518        impl = _test_interpreter_version_info_parses_values_to_struct_impl,
519        expect_failure = False,
520    )
521
522def _test_interpreter_version_info_parses_values_to_struct_impl(env, target):
523    version_info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject).interpreter_version_info()
524    version_info.major().equals(3)
525    version_info.minor().equals(6)
526    version_info.micro().equals(10)
527    version_info.releaselevel().equals("alpha")
528    version_info.serial().equals(1)
529
530_tests.append(_test_interpreter_version_info_parses_values_to_struct)
531
532def _test_version_info_from_flag(name):
533    if not config.enable_pystar:
534        rt_util.skip_test(name)
535        return
536    py_runtime(
537        name = name + "_subject",
538        interpreter_version_info = None,
539        interpreter_path = "/bogus",
540    )
541    analysis_test(
542        name = name,
543        target = name + "_subject",
544        impl = _test_version_info_from_flag_impl,
545        config_settings = {
546            PYTHON_VERSION: "3.12",
547        },
548    )
549
550def _test_version_info_from_flag_impl(env, target):
551    version_info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject).interpreter_version_info()
552    version_info.major().equals(3)
553    version_info.minor().equals(12)
554    version_info.micro().equals(None)
555    version_info.releaselevel().equals(None)
556    version_info.serial().equals(None)
557
558_tests.append(_test_version_info_from_flag)
559
560def py_runtime_test_suite(name):
561    test_suite(
562        name = name,
563        tests = _tests,
564    )
565