# Copyright 2024 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for precompiling behavior.""" load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:py_binary.bzl", "py_binary") load("//python:py_info.bzl", "PyInfo") load("//python:py_library.bzl", "py_library") load("//python:py_test.bzl", "py_test") load("//tests/support:py_info_subject.bzl", "py_info_subject") load( "//tests/support:support.bzl", "CC_TOOLCHAIN", "EXEC_TOOLS_TOOLCHAIN", "PRECOMPILE", "PRECOMPILE_ADD_TO_RUNFILES", "PRECOMPILE_SOURCE_RETENTION", "PY_TOOLCHAINS", ) _COMMON_CONFIG_SETTINGS = { # This isn't enabled in all environments the tests run in, so disable # it for conformity. "//command_line_option:allow_unresolved_symlinks": True, "//command_line_option:extra_toolchains": [PY_TOOLCHAINS, CC_TOOLCHAIN], EXEC_TOOLS_TOOLCHAIN: "enabled", } _tests = [] def _test_precompile_enabled_setup(name, py_rule, **kwargs): if not rp_config.enable_pystar: rt_util.skip_test(name = name) return rt_util.helper_target( py_rule, name = name + "_subject", precompile = "enabled", srcs = ["main.py"], deps = [name + "_lib"], **kwargs ) rt_util.helper_target( py_library, name = name + "_lib", srcs = ["lib.py"], precompile = "enabled", ) analysis_test( name = name, impl = _test_precompile_enabled_impl, target = name + "_subject", config_settings = _COMMON_CONFIG_SETTINGS, ) def _test_precompile_enabled_impl(env, target): target = env.expect.that_target(target) runfiles = target.runfiles() runfiles.contains_predicate( matching.str_matches("__pycache__/main.fakepy-45.pyc"), ) runfiles.contains_predicate( matching.str_matches("/main.py"), ) target.default_outputs().contains_at_least_predicates([ matching.file_path_matches("__pycache__/main.fakepy-45.pyc"), matching.file_path_matches("/main.py"), ]) py_info = target.provider(PyInfo, factory = py_info_subject) py_info.direct_pyc_files().contains_exactly([ "{package}/__pycache__/main.fakepy-45.pyc", ]) py_info.transitive_pyc_files().contains_exactly([ "{package}/__pycache__/main.fakepy-45.pyc", "{package}/__pycache__/lib.fakepy-45.pyc", ]) def _test_precompile_enabled_py_binary(name): _test_precompile_enabled_setup(name = name, py_rule = py_binary, main = "main.py") _tests.append(_test_precompile_enabled_py_binary) def _test_precompile_enabled_py_test(name): _test_precompile_enabled_setup(name = name, py_rule = py_test, main = "main.py") _tests.append(_test_precompile_enabled_py_test) def _test_precompile_enabled_py_library(name): _test_precompile_enabled_setup(name = name, py_rule = py_library) _tests.append(_test_precompile_enabled_py_library) def _test_pyc_only(name): if not rp_config.enable_pystar: rt_util.skip_test(name = name) return rt_util.helper_target( py_binary, name = name + "_subject", precompile = "enabled", srcs = ["main.py"], main = "main.py", precompile_source_retention = "omit_source", ) analysis_test( name = name, impl = _test_pyc_only_impl, config_settings = _COMMON_CONFIG_SETTINGS | { ##PRECOMPILE_SOURCE_RETENTION: "omit_source", PRECOMPILE: "enabled", }, target = name + "_subject", ) _tests.append(_test_pyc_only) def _test_pyc_only_impl(env, target): target = env.expect.that_target(target) runfiles = target.runfiles() runfiles.contains_predicate( matching.str_matches("/main.pyc"), ) runfiles.not_contains_predicate( matching.str_endswith("/main.py"), ) target.default_outputs().contains_at_least_predicates([ matching.file_path_matches("/main.pyc"), ]) target.default_outputs().not_contains_predicate( matching.file_basename_equals("main.py"), ) def _test_precompile_if_generated(name): if not rp_config.enable_pystar: rt_util.skip_test(name = name) return rt_util.helper_target( py_binary, name = name + "_subject", srcs = [ "main.py", rt_util.empty_file("generated1.py"), ], main = "main.py", precompile = "if_generated_source", ) analysis_test( name = name, impl = _test_precompile_if_generated_impl, target = name + "_subject", config_settings = _COMMON_CONFIG_SETTINGS, ) _tests.append(_test_precompile_if_generated) def _test_precompile_if_generated_impl(env, target): target = env.expect.that_target(target) runfiles = target.runfiles() runfiles.contains_predicate( matching.str_matches("/__pycache__/generated1.fakepy-45.pyc"), ) runfiles.not_contains_predicate( matching.str_matches("main.*pyc"), ) target.default_outputs().contains_at_least_predicates([ matching.file_path_matches("/__pycache__/generated1.fakepy-45.pyc"), ]) target.default_outputs().not_contains_predicate( matching.file_path_matches("main.*pyc"), ) def _test_omit_source_if_generated_source(name): if not rp_config.enable_pystar: rt_util.skip_test(name = name) return rt_util.helper_target( py_binary, name = name + "_subject", srcs = [ "main.py", rt_util.empty_file("generated2.py"), ], main = "main.py", precompile = "enabled", ) analysis_test( name = name, impl = _test_omit_source_if_generated_source_impl, target = name + "_subject", config_settings = _COMMON_CONFIG_SETTINGS | { PRECOMPILE_SOURCE_RETENTION: "omit_if_generated_source", }, ) _tests.append(_test_omit_source_if_generated_source) def _test_omit_source_if_generated_source_impl(env, target): target = env.expect.that_target(target) runfiles = target.runfiles() runfiles.contains_predicate( matching.str_matches("/generated2.pyc"), ) runfiles.contains_predicate( matching.str_matches("__pycache__/main.fakepy-45.pyc"), ) target.default_outputs().contains_at_least_predicates([ matching.file_path_matches("generated2.pyc"), ]) target.default_outputs().contains_predicate( matching.file_path_matches("__pycache__/main.fakepy-45.pyc"), ) def _test_precompile_add_to_runfiles_decided_elsewhere(name): if not rp_config.enable_pystar: rt_util.skip_test(name = name) return rt_util.helper_target( py_binary, name = name + "_binary", srcs = ["bin.py"], main = "bin.py", deps = [name + "_lib"], pyc_collection = "include_pyc", ) rt_util.helper_target( py_library, name = name + "_lib", srcs = ["lib.py"], ) analysis_test( name = name, impl = _test_precompile_add_to_runfiles_decided_elsewhere_impl, targets = { "binary": name + "_binary", "library": name + "_lib", }, config_settings = _COMMON_CONFIG_SETTINGS | { PRECOMPILE_ADD_TO_RUNFILES: "decided_elsewhere", PRECOMPILE: "enabled", }, ) _tests.append(_test_precompile_add_to_runfiles_decided_elsewhere) def _test_precompile_add_to_runfiles_decided_elsewhere_impl(env, targets): env.expect.that_target(targets.binary).runfiles().contains_at_least([ "{workspace}/{package}/__pycache__/bin.fakepy-45.pyc", "{workspace}/{package}/__pycache__/lib.fakepy-45.pyc", "{workspace}/{package}/bin.py", "{workspace}/{package}/lib.py", ]) env.expect.that_target(targets.library).runfiles().contains_exactly([ "{workspace}/{package}/lib.py", ]) def _test_precompiler_action(name): if not rp_config.enable_pystar: rt_util.skip_test(name = name) return rt_util.helper_target( py_binary, name = name + "_subject", srcs = ["main2.py"], main = "main2.py", precompile = "enabled", precompile_optimize_level = 2, precompile_invalidation_mode = "unchecked_hash", ) analysis_test( name = name, impl = _test_precompiler_action_impl, target = name + "_subject", config_settings = _COMMON_CONFIG_SETTINGS, ) _tests.append(_test_precompiler_action) def _test_precompiler_action_impl(env, target): action = env.expect.that_target(target).action_named("PyCompile") action.contains_flag_values([ ("--optimize", "2"), ("--python_version", "4.5"), ("--invalidation_mode", "unchecked_hash"), ]) action.has_flags_specified(["--src", "--pyc", "--src_name"]) action.env().contains_at_least({ "PYTHONHASHSEED": "0", "PYTHONNOUSERSITE": "1", "PYTHONSAFEPATH": "1", }) def precompile_test_suite(name): test_suite( name = name, tests = _tests, )