1# Copyright 2022 Google LLC. 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"""A framework for writing tests of Starlark code with minimal overhead. 16 17Test cases are written as Starlark functions that will eventually be executed as 18individual test targets. Two types of tests are supported: those including their 19own assertions, and those expected to call 'fail()'. 20 21Basic usage looks like: 22``` 23# unittests.bzl 24load("//kotlin/common/testing:unittest_suites.bzl", "kt_unittest_suites") 25load("@bazel_skylib//lib:unittest.bzl", "asserts") 26 27unittests = kt_unittest_suite.create() # Create a new suite in this file. 28 29def _some_test_case(ctx, env): 30 # Test logic here 31 asserts.true(env, 1 == 1) 32 return [] # Return any declared files 33 34unittests.expect_finish(_some_test_case) # Include the test case in the suite 35 36def _some_fail_case(ctx): 37 # No assertions are allowed in fail cases 38 _some_logic_that_should_call_fail(ctx) 39 40unittests.expect_fail(_some_fail_case, "fail message substring") # Expect this case to call fail 41 42# Generate a pair of rules that will be used for test targets 43_test, _fail = unittests.close() # @unused 44``` 45 46``` 47// BUILD 48load(":unittests.bzl", "unittests") 49 50# Render each test case as a target in this package 51unittests.render( 52 name = "unittests" 53) 54``` 55""" 56 57load("//:visibility.bzl", "RULES_KOTLIN") 58load("@bazel_skylib//lib:unittest.bzl", "unittest") 59load(":testing_rules.bzl", "kt_testing_rules") 60 61visibility(RULES_KOTLIN) 62 63def _create(): 64 """Create a new test suite. 65 66 Returns: 67 [kt_unittest_suite] An object representing the suite under construction 68 """ 69 70 test_cases = dict() 71 rule_holder = [] # Use a list rather than separate vars becase captured vars are final 72 73 def expect_fail(test_case, msg_contains): 74 """Add a test case to the suite which is expected to call fail. 75 76 Args: 77 test_case: [function(ctx)] 78 msg_contains: [string] A substring expected in the failure message 79 """ 80 81 if rule_holder: 82 fail("Test suite is closed") 83 84 test_case_name = _fn_name(test_case) 85 if not test_case_name.startswith("_"): 86 fail("Test cases must be private '%s'" % test_case_name) 87 if test_case_name in test_cases: 88 fail("Existing test case named '%s'" % test_case_name) 89 90 test_cases[test_case_name] = struct( 91 impl = test_case, 92 msg_contains = msg_contains, 93 ) 94 95 def expect_finish(test_case): 96 """Add a test case to the suite. 97 98 Args: 99 test_case: [function(ctx,unittest.env):None|list[File]] 100 """ 101 102 expect_fail(test_case, None) 103 104 def close(): 105 """Close the suite from expect_finishing new tests. 106 107 The return value must be assigned to '_test, _fail' with an '# @unused' suppression. 108 109 Returns: 110 [(rule, rule)] 111 """ 112 113 if rule_holder: 114 fail("Test suite is closed") 115 116 def test_impl(ctx): 117 env = unittest.begin(ctx) 118 119 output_files = test_cases[ctx.attr.case_name].impl(ctx, env) or [] 120 if output_files: 121 ctx.actions.run_shell( 122 outputs = output_files, 123 command = "exit 1", 124 ) 125 126 return unittest.end(env) + [OutputGroupInfo(_file_sink = depset(output_files))] 127 128 test_rule = unittest.make( 129 impl = test_impl, 130 attrs = dict(case_name = attr.string()), 131 ) 132 rule_holder.append(test_rule) 133 134 def fail_impl(ctx): 135 test_cases[ctx.attr.case_name].impl(ctx) 136 return [] 137 138 fail_rule = rule( 139 implementation = fail_impl, 140 attrs = dict(case_name = attr.string()), 141 ) 142 rule_holder.append(fail_rule) 143 144 # Rules must be assigned to top-level Starlark vars before being called 145 return test_rule, fail_rule 146 147 def render(name, tags = [], **kwargs): 148 """Render the test suite into targets. 149 150 Args: 151 name: [string] 152 tags: [list[string]] 153 **kwargs: Generic rule kwargs 154 """ 155 156 if not rule_holder: 157 fail("Test suite is not closed") 158 test_rule = rule_holder[0] 159 fail_rule = rule_holder[1] 160 161 test_targets = [] 162 for test_case_name, test_case_data in test_cases.items(): 163 target_name = test_case_name.removeprefix("_") + "_test" 164 test_targets.append(target_name) 165 166 if test_case_data.msg_contains == None: 167 test_rule( 168 name = target_name, 169 tags = tags, 170 case_name = test_case_name, 171 **kwargs 172 ) 173 else: 174 fail_rule( 175 name = test_case_name, 176 tags = tags + kt_testing_rules.ONLY_FOR_ANALYSIS_TAGS, 177 case_name = test_case_name, 178 **kwargs 179 ) 180 kt_testing_rules.assert_failure_test( 181 name = target_name, 182 target_under_test = test_case_name, 183 msg_contains = test_case_data.msg_contains, 184 ) 185 186 native.test_suite( 187 name = name, 188 tests = test_targets, 189 **kwargs 190 ) 191 192 return struct( 193 expect_finish = expect_finish, 194 expect_fail = expect_fail, 195 close = close, 196 render = render, 197 ) 198 199def _fn_name(rule_or_fn): 200 parts = str(rule_or_fn).removeprefix("<").removesuffix(">").split(" ") 201 return parts[0] if (len(parts) == 1) else parts[1] 202 203kt_unittest_suites = struct( 204 create = _create, 205) 206