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"""Tests for precompiling behavior.""" 16 17load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") 18load("@rules_testing//lib:analysis_test.bzl", "analysis_test") 19load("@rules_testing//lib:test_suite.bzl", "test_suite") 20load("@rules_testing//lib:truth.bzl", "matching") 21load("@rules_testing//lib:util.bzl", rt_util = "util") 22load("//python:py_binary.bzl", "py_binary") 23load("//python:py_info.bzl", "PyInfo") 24load("//python:py_library.bzl", "py_library") 25load("//python:py_test.bzl", "py_test") 26load("//tests/support:py_info_subject.bzl", "py_info_subject") 27load( 28 "//tests/support:support.bzl", 29 "CC_TOOLCHAIN", 30 "EXEC_TOOLS_TOOLCHAIN", 31 "PRECOMPILE", 32 "PRECOMPILE_ADD_TO_RUNFILES", 33 "PRECOMPILE_SOURCE_RETENTION", 34 "PY_TOOLCHAINS", 35) 36 37_COMMON_CONFIG_SETTINGS = { 38 # This isn't enabled in all environments the tests run in, so disable 39 # it for conformity. 40 "//command_line_option:allow_unresolved_symlinks": True, 41 "//command_line_option:extra_toolchains": [PY_TOOLCHAINS, CC_TOOLCHAIN], 42 EXEC_TOOLS_TOOLCHAIN: "enabled", 43} 44 45_tests = [] 46 47def _test_precompile_enabled_setup(name, py_rule, **kwargs): 48 if not rp_config.enable_pystar: 49 rt_util.skip_test(name = name) 50 return 51 rt_util.helper_target( 52 py_rule, 53 name = name + "_subject", 54 precompile = "enabled", 55 srcs = ["main.py"], 56 deps = [name + "_lib"], 57 **kwargs 58 ) 59 rt_util.helper_target( 60 py_library, 61 name = name + "_lib", 62 srcs = ["lib.py"], 63 precompile = "enabled", 64 ) 65 analysis_test( 66 name = name, 67 impl = _test_precompile_enabled_impl, 68 target = name + "_subject", 69 config_settings = _COMMON_CONFIG_SETTINGS, 70 ) 71 72def _test_precompile_enabled_impl(env, target): 73 target = env.expect.that_target(target) 74 runfiles = target.runfiles() 75 runfiles.contains_predicate( 76 matching.str_matches("__pycache__/main.fakepy-45.pyc"), 77 ) 78 runfiles.contains_predicate( 79 matching.str_matches("/main.py"), 80 ) 81 target.default_outputs().contains_at_least_predicates([ 82 matching.file_path_matches("__pycache__/main.fakepy-45.pyc"), 83 matching.file_path_matches("/main.py"), 84 ]) 85 py_info = target.provider(PyInfo, factory = py_info_subject) 86 py_info.direct_pyc_files().contains_exactly([ 87 "{package}/__pycache__/main.fakepy-45.pyc", 88 ]) 89 py_info.transitive_pyc_files().contains_exactly([ 90 "{package}/__pycache__/main.fakepy-45.pyc", 91 "{package}/__pycache__/lib.fakepy-45.pyc", 92 ]) 93 94def _test_precompile_enabled_py_binary(name): 95 _test_precompile_enabled_setup(name = name, py_rule = py_binary, main = "main.py") 96 97_tests.append(_test_precompile_enabled_py_binary) 98 99def _test_precompile_enabled_py_test(name): 100 _test_precompile_enabled_setup(name = name, py_rule = py_test, main = "main.py") 101 102_tests.append(_test_precompile_enabled_py_test) 103 104def _test_precompile_enabled_py_library(name): 105 _test_precompile_enabled_setup(name = name, py_rule = py_library) 106 107_tests.append(_test_precompile_enabled_py_library) 108 109def _test_pyc_only(name): 110 if not rp_config.enable_pystar: 111 rt_util.skip_test(name = name) 112 return 113 rt_util.helper_target( 114 py_binary, 115 name = name + "_subject", 116 precompile = "enabled", 117 srcs = ["main.py"], 118 main = "main.py", 119 precompile_source_retention = "omit_source", 120 ) 121 analysis_test( 122 name = name, 123 impl = _test_pyc_only_impl, 124 config_settings = _COMMON_CONFIG_SETTINGS | { 125 ##PRECOMPILE_SOURCE_RETENTION: "omit_source", 126 PRECOMPILE: "enabled", 127 }, 128 target = name + "_subject", 129 ) 130 131_tests.append(_test_pyc_only) 132 133def _test_pyc_only_impl(env, target): 134 target = env.expect.that_target(target) 135 runfiles = target.runfiles() 136 runfiles.contains_predicate( 137 matching.str_matches("/main.pyc"), 138 ) 139 runfiles.not_contains_predicate( 140 matching.str_endswith("/main.py"), 141 ) 142 target.default_outputs().contains_at_least_predicates([ 143 matching.file_path_matches("/main.pyc"), 144 ]) 145 target.default_outputs().not_contains_predicate( 146 matching.file_basename_equals("main.py"), 147 ) 148 149def _test_precompile_if_generated(name): 150 if not rp_config.enable_pystar: 151 rt_util.skip_test(name = name) 152 return 153 rt_util.helper_target( 154 py_binary, 155 name = name + "_subject", 156 srcs = [ 157 "main.py", 158 rt_util.empty_file("generated1.py"), 159 ], 160 main = "main.py", 161 precompile = "if_generated_source", 162 ) 163 analysis_test( 164 name = name, 165 impl = _test_precompile_if_generated_impl, 166 target = name + "_subject", 167 config_settings = _COMMON_CONFIG_SETTINGS, 168 ) 169 170_tests.append(_test_precompile_if_generated) 171 172def _test_precompile_if_generated_impl(env, target): 173 target = env.expect.that_target(target) 174 runfiles = target.runfiles() 175 runfiles.contains_predicate( 176 matching.str_matches("/__pycache__/generated1.fakepy-45.pyc"), 177 ) 178 runfiles.not_contains_predicate( 179 matching.str_matches("main.*pyc"), 180 ) 181 target.default_outputs().contains_at_least_predicates([ 182 matching.file_path_matches("/__pycache__/generated1.fakepy-45.pyc"), 183 ]) 184 target.default_outputs().not_contains_predicate( 185 matching.file_path_matches("main.*pyc"), 186 ) 187 188def _test_omit_source_if_generated_source(name): 189 if not rp_config.enable_pystar: 190 rt_util.skip_test(name = name) 191 return 192 rt_util.helper_target( 193 py_binary, 194 name = name + "_subject", 195 srcs = [ 196 "main.py", 197 rt_util.empty_file("generated2.py"), 198 ], 199 main = "main.py", 200 precompile = "enabled", 201 ) 202 analysis_test( 203 name = name, 204 impl = _test_omit_source_if_generated_source_impl, 205 target = name + "_subject", 206 config_settings = _COMMON_CONFIG_SETTINGS | { 207 PRECOMPILE_SOURCE_RETENTION: "omit_if_generated_source", 208 }, 209 ) 210 211_tests.append(_test_omit_source_if_generated_source) 212 213def _test_omit_source_if_generated_source_impl(env, target): 214 target = env.expect.that_target(target) 215 runfiles = target.runfiles() 216 runfiles.contains_predicate( 217 matching.str_matches("/generated2.pyc"), 218 ) 219 runfiles.contains_predicate( 220 matching.str_matches("__pycache__/main.fakepy-45.pyc"), 221 ) 222 target.default_outputs().contains_at_least_predicates([ 223 matching.file_path_matches("generated2.pyc"), 224 ]) 225 target.default_outputs().contains_predicate( 226 matching.file_path_matches("__pycache__/main.fakepy-45.pyc"), 227 ) 228 229def _test_precompile_add_to_runfiles_decided_elsewhere(name): 230 if not rp_config.enable_pystar: 231 rt_util.skip_test(name = name) 232 return 233 rt_util.helper_target( 234 py_binary, 235 name = name + "_binary", 236 srcs = ["bin.py"], 237 main = "bin.py", 238 deps = [name + "_lib"], 239 pyc_collection = "include_pyc", 240 ) 241 rt_util.helper_target( 242 py_library, 243 name = name + "_lib", 244 srcs = ["lib.py"], 245 ) 246 analysis_test( 247 name = name, 248 impl = _test_precompile_add_to_runfiles_decided_elsewhere_impl, 249 targets = { 250 "binary": name + "_binary", 251 "library": name + "_lib", 252 }, 253 config_settings = _COMMON_CONFIG_SETTINGS | { 254 PRECOMPILE_ADD_TO_RUNFILES: "decided_elsewhere", 255 PRECOMPILE: "enabled", 256 }, 257 ) 258 259_tests.append(_test_precompile_add_to_runfiles_decided_elsewhere) 260 261def _test_precompile_add_to_runfiles_decided_elsewhere_impl(env, targets): 262 env.expect.that_target(targets.binary).runfiles().contains_at_least([ 263 "{workspace}/{package}/__pycache__/bin.fakepy-45.pyc", 264 "{workspace}/{package}/__pycache__/lib.fakepy-45.pyc", 265 "{workspace}/{package}/bin.py", 266 "{workspace}/{package}/lib.py", 267 ]) 268 269 env.expect.that_target(targets.library).runfiles().contains_exactly([ 270 "{workspace}/{package}/lib.py", 271 ]) 272 273def _test_precompiler_action(name): 274 if not rp_config.enable_pystar: 275 rt_util.skip_test(name = name) 276 return 277 rt_util.helper_target( 278 py_binary, 279 name = name + "_subject", 280 srcs = ["main2.py"], 281 main = "main2.py", 282 precompile = "enabled", 283 precompile_optimize_level = 2, 284 precompile_invalidation_mode = "unchecked_hash", 285 ) 286 analysis_test( 287 name = name, 288 impl = _test_precompiler_action_impl, 289 target = name + "_subject", 290 config_settings = _COMMON_CONFIG_SETTINGS, 291 ) 292 293_tests.append(_test_precompiler_action) 294 295def _test_precompiler_action_impl(env, target): 296 action = env.expect.that_target(target).action_named("PyCompile") 297 action.contains_flag_values([ 298 ("--optimize", "2"), 299 ("--python_version", "4.5"), 300 ("--invalidation_mode", "unchecked_hash"), 301 ]) 302 action.has_flags_specified(["--src", "--pyc", "--src_name"]) 303 action.env().contains_at_least({ 304 "PYTHONHASHSEED": "0", 305 "PYTHONNOUSERSITE": "1", 306 "PYTHONSAFEPATH": "1", 307 }) 308 309def precompile_test_suite(name): 310 test_suite( 311 name = name, 312 tests = _tests, 313 ) 314