| # 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. | |
| """Implementation of a result type for use with rules_testing.""" | |
| load("@bazel_skylib//lib:structs.bzl", "structs") | |
| load("@rules_testing//lib:truth.bzl", "subjects") | |
| visibility("//tests/rule_based_toolchain/...") | |
| def result_fn_wrapper(fn): | |
| """Wraps a function that may fail in a type similar to rust's Result type. | |
| An example usage is the following: | |
| # Implementation file | |
| def get_only(value, fail=fail): | |
| if len(value) == 1: | |
| return value[0] | |
| elif not value: | |
| fail("Unexpectedly empty") | |
| else: | |
| fail("%r had length %d, expected 1" % (value, len(value)) | |
| # Test file | |
| load("...", _fn=fn) | |
| fn = result_fn_wrapper(_fn) | |
| int_result = result_subject(subjects.int) | |
| def my_test(env, _): | |
| env.expect.that_value(fn([]), factory=int_result) | |
| .err().equals("Unexpectedly empty") | |
| env.expect.that_value(fn([1]), factory=int_result) | |
| .ok().equals(1) | |
| env.expect.that_value(fn([1, 2]), factory=int_result) | |
| .err().contains("had length 2, expected 1") | |
| Args: | |
| fn: A function that takes in a parameter fail and calls it on failure. | |
| Returns: | |
| On success: struct(ok = <result>, err = None) | |
| On failure: struct(ok = None, err = <first error message> | |
| """ | |
| def new_fn(*args, **kwargs): | |
| # Use a mutable type so that the fail_wrapper can modify this. | |
| failures = [] | |
| def fail_wrapper(msg): | |
| failures.append(msg) | |
| result = fn(fail = fail_wrapper, *args, **kwargs) | |
| if failures: | |
| return struct(ok = None, err = failures[0]) | |
| else: | |
| return struct(ok = result, err = None) | |
| return new_fn | |
| def result_subject(factory): | |
| """A subject factory for Result<T>. | |
| Args: | |
| factory: A subject factory for T | |
| Returns: | |
| A subject factory for Result<T> | |
| """ | |
| def new_factory(value, *, meta): | |
| def ok(): | |
| if value.err != None: | |
| meta.add_failure("Wanted a value, but got an error", value.err) | |
| return factory(value.ok, meta = meta.derive("ok()")) | |
| def err(): | |
| if value.err == None: | |
| meta.add_failure("Wanted an error, but got a value", value.ok) | |
| subject = subjects.str(value.err, meta = meta.derive("err()")) | |
| def contains_all_of(values): | |
| for value in values: | |
| subject.contains(str(value)) | |
| return struct(contains_all_of = contains_all_of, **structs.to_dict(subject)) | |
| return struct(ok = ok, err = err) | |
| return new_factory | |
| def optional_subject(factory): | |
| """A subject factory for Optional<T>. | |
| Args: | |
| factory: A subject factory for T | |
| Returns: | |
| A subject factory for Optional<T> | |
| """ | |
| def new_factory(value, *, meta): | |
| def some(): | |
| if value == None: | |
| meta.add_failure("Wanted a value, but got None", None) | |
| return factory(value, meta = meta) | |
| def is_none(): | |
| if value != None: | |
| meta.add_failure("Wanted None, but got a value", value) | |
| return struct(some = some, is_none = is_none) | |
| return new_factory | |
| # Curry subjects.struct so the type is actually generic. | |
| struct_subject = lambda **attrs: lambda value, *, meta: subjects.struct( | |
| value, | |
| meta = meta, | |
| attrs = attrs, | |
| ) | |
| # We can't do complex assertions on containers. This allows you to write | |
| # assert.that_value({"foo": 1), factory=dict_key_subject(subjects.int)) | |
| # .get("foo").equals(1) | |
| dict_key_subject = lambda factory: lambda value, *, meta: struct( | |
| get = lambda key: factory( | |
| value[key], | |
| meta = meta.derive("get({})".format(key)), | |
| ), | |
| keys = lambda: subjects.collection(value.keys(), meta = meta.derive("keys()")), | |
| contains = lambda key: subjects.bool(key in value, meta = meta.derive("contains({})".format(key))), | |
| ) |