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"""Tests common to py_binary and py_test (executable rules).""" 15 16load("@rules_testing//lib:analysis_test.bzl", "analysis_test") 17load("@rules_testing//lib:truth.bzl", "matching") 18load("@rules_testing//lib:util.bzl", rt_util = "util") 19load("//tools/build_defs/python/tests:base_tests.bzl", "create_base_tests") 20load("//tools/build_defs/python/tests:util.bzl", "WINDOWS_ATTR", pt_util = "util") 21 22_tests = [] 23 24def _test_executable_in_runfiles(name, config): 25 rt_util.helper_target( 26 config.rule, 27 name = name + "_subject", 28 srcs = [name + "_subject.py"], 29 ) 30 analysis_test( 31 name = name, 32 impl = _test_executable_in_runfiles_impl, 33 target = name + "_subject", 34 attrs = WINDOWS_ATTR, 35 ) 36 37_tests.append(_test_executable_in_runfiles) 38 39def _test_executable_in_runfiles_impl(env, target): 40 if pt_util.is_windows(env): 41 exe = ".exe" 42 else: 43 exe = "" 44 45 env.expect.that_target(target).runfiles().contains_at_least([ 46 "{workspace}/{package}/{test_name}_subject" + exe, 47 ]) 48 49def _test_default_main_can_be_generated(name, config): 50 rt_util.helper_target( 51 config.rule, 52 name = name + "_subject", 53 srcs = [rt_util.empty_file(name + "_subject.py")], 54 ) 55 analysis_test( 56 name = name, 57 impl = _test_default_main_can_be_generated_impl, 58 target = name + "_subject", 59 ) 60 61_tests.append(_test_default_main_can_be_generated) 62 63def _test_default_main_can_be_generated_impl(env, target): 64 env.expect.that_target(target).default_outputs().contains( 65 "{package}/{test_name}_subject.py", 66 ) 67 68def _test_default_main_can_have_multiple_path_segments(name, config): 69 rt_util.helper_target( 70 config.rule, 71 name = name + "/subject", 72 srcs = [name + "/subject.py"], 73 ) 74 analysis_test( 75 name = name, 76 impl = _test_default_main_can_have_multiple_path_segments_impl, 77 target = name + "/subject", 78 ) 79 80_tests.append(_test_default_main_can_have_multiple_path_segments) 81 82def _test_default_main_can_have_multiple_path_segments_impl(env, target): 83 env.expect.that_target(target).default_outputs().contains( 84 "{package}/{test_name}/subject.py", 85 ) 86 87def _test_default_main_must_be_in_srcs(name, config): 88 # Bazel 5 will crash with a Java stacktrace when the native Python 89 # rules have an error. 90 if not pt_util.is_bazel_6_or_higher(): 91 rt_util.skip_test(name = name) 92 return 93 rt_util.helper_target( 94 config.rule, 95 name = name + "_subject", 96 srcs = ["other.py"], 97 ) 98 analysis_test( 99 name = name, 100 impl = _test_default_main_must_be_in_srcs_impl, 101 target = name + "_subject", 102 expect_failure = True, 103 ) 104 105_tests.append(_test_default_main_must_be_in_srcs) 106 107def _test_default_main_must_be_in_srcs_impl(env, target): 108 env.expect.that_target(target).failures().contains_predicate( 109 matching.str_matches("default*does not appear in srcs"), 110 ) 111 112def _test_default_main_cannot_be_ambiguous(name, config): 113 # Bazel 5 will crash with a Java stacktrace when the native Python 114 # rules have an error. 115 if not pt_util.is_bazel_6_or_higher(): 116 rt_util.skip_test(name = name) 117 return 118 rt_util.helper_target( 119 config.rule, 120 name = name + "_subject", 121 srcs = [name + "_subject.py", "other/{}_subject.py".format(name)], 122 ) 123 analysis_test( 124 name = name, 125 impl = _test_default_main_cannot_be_ambiguous_impl, 126 target = name + "_subject", 127 expect_failure = True, 128 ) 129 130_tests.append(_test_default_main_cannot_be_ambiguous) 131 132def _test_default_main_cannot_be_ambiguous_impl(env, target): 133 env.expect.that_target(target).failures().contains_predicate( 134 matching.str_matches("default main*matches multiple files"), 135 ) 136 137def _test_explicit_main(name, config): 138 rt_util.helper_target( 139 config.rule, 140 name = name + "_subject", 141 srcs = ["custom.py"], 142 main = "custom.py", 143 ) 144 analysis_test( 145 name = name, 146 impl = _test_explicit_main_impl, 147 target = name + "_subject", 148 ) 149 150_tests.append(_test_explicit_main) 151 152def _test_explicit_main_impl(env, target): 153 # There isn't a direct way to ask what main file was selected, so we 154 # rely on it being in the default outputs. 155 env.expect.that_target(target).default_outputs().contains( 156 "{package}/custom.py", 157 ) 158 159def _test_explicit_main_cannot_be_ambiguous(name, config): 160 # Bazel 5 will crash with a Java stacktrace when the native Python 161 # rules have an error. 162 if not pt_util.is_bazel_6_or_higher(): 163 rt_util.skip_test(name = name) 164 return 165 rt_util.helper_target( 166 config.rule, 167 name = name + "_subject", 168 srcs = ["x/foo.py", "y/foo.py"], 169 main = "foo.py", 170 ) 171 analysis_test( 172 name = name, 173 impl = _test_explicit_main_cannot_be_ambiguous_impl, 174 target = name + "_subject", 175 expect_failure = True, 176 ) 177 178_tests.append(_test_explicit_main_cannot_be_ambiguous) 179 180def _test_explicit_main_cannot_be_ambiguous_impl(env, target): 181 env.expect.that_target(target).failures().contains_predicate( 182 matching.str_matches("foo.py*matches multiple"), 183 ) 184 185def _test_files_to_build(name, config): 186 rt_util.helper_target( 187 config.rule, 188 name = name + "_subject", 189 srcs = [name + "_subject.py"], 190 ) 191 analysis_test( 192 name = name, 193 impl = _test_files_to_build_impl, 194 target = name + "_subject", 195 attrs = WINDOWS_ATTR, 196 ) 197 198_tests.append(_test_files_to_build) 199 200def _test_files_to_build_impl(env, target): 201 default_outputs = env.expect.that_target(target).default_outputs() 202 if pt_util.is_windows(env): 203 default_outputs.contains("{package}/{test_name}_subject.exe") 204 else: 205 default_outputs.contains_exactly([ 206 "{package}/{test_name}_subject", 207 "{package}/{test_name}_subject.py", 208 ]) 209 210def _test_name_cannot_end_in_py(name, config): 211 # Bazel 5 will crash with a Java stacktrace when the native Python 212 # rules have an error. 213 if not pt_util.is_bazel_6_or_higher(): 214 rt_util.skip_test(name = name) 215 return 216 rt_util.helper_target( 217 config.rule, 218 name = name + "_subject.py", 219 srcs = ["main.py"], 220 ) 221 analysis_test( 222 name = name, 223 impl = _test_name_cannot_end_in_py_impl, 224 target = name + "_subject.py", 225 expect_failure = True, 226 ) 227 228_tests.append(_test_name_cannot_end_in_py) 229 230def _test_name_cannot_end_in_py_impl(env, target): 231 env.expect.that_target(target).failures().contains_predicate( 232 matching.str_matches("name must not end in*.py"), 233 ) 234 235# Can't test this -- mandatory validation happens before analysis test 236# can intercept it 237# TODO(#1069): Once re-implemented in Starlark, modify rule logic to make this 238# testable. 239# def _test_srcs_is_mandatory(name, config): 240# rt_util.helper_target( 241# config.rule, 242# name = name + "_subject", 243# ) 244# analysis_test( 245# name = name, 246# impl = _test_srcs_is_mandatory, 247# target = name + "_subject", 248# expect_failure = True, 249# ) 250# 251# _tests.append(_test_srcs_is_mandatory) 252# 253# def _test_srcs_is_mandatory_impl(env, target): 254# env.expect.that_target(target).failures().contains_predicate( 255# matching.str_matches("mandatory*srcs"), 256# ) 257 258# ===== 259# You were gonna add a test at the end, weren't you? 260# Nope. Please keep them sorted; put it in its alphabetical location. 261# Here's the alphabet so you don't have to sing that song in your head: 262# A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 263# ===== 264 265def create_executable_tests(config): 266 def _executable_with_srcs_wrapper(name, **kwargs): 267 if not kwargs.get("srcs"): 268 kwargs["srcs"] = [name + ".py"] 269 config.rule(name = name, **kwargs) 270 271 config = pt_util.struct_with(config, base_test_rule = _executable_with_srcs_wrapper) 272 return pt_util.create_tests(_tests, config = config) + create_base_tests(config = config) 273