xref: /aosp_15_r20/external/bazelbuild-kotlin-rules/kotlin/common/testing/unittest_suites.bzl (revision 3a22c0a33dd99bcca39a024d43e6fbcc55c2806e)
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