xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/truth_common.bzl (revision d605057434dcabba796c020773aab68d9790ff9f)
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
15"""Common code used by truth."""
16
17load("@bazel_skylib//lib:types.bzl", "types")
18
19def mkmethod(self, method):
20    """Bind a struct as the first arg to a function.
21
22    This is loosely equivalent to creating a bound method of a class.
23    """
24    return lambda *args, **kwargs: method(self, *args, **kwargs)
25
26def repr_with_type(value):
27    return "<{} {}>".format(type(value), repr(value))
28
29def _informative_str(value):
30    value_str = str(value)
31    if not value_str:
32        return "<empty string ∅>"
33    elif "\n" in value_str:
34        return '"""{}""" <sans triple-quotes; note newlines and whitespace>'.format(value_str)
35    elif value_str != value_str.strip():
36        return '"{}" <sans quotes; note whitespace within>'.format(value_str)
37    else:
38        return value_str
39
40def enumerate_list_as_lines(values, prefix = "", format_value = None):
41    """Format a list of values in a human-friendly list.
42
43    Args:
44        values: ([`list`]) the values to display, one per line.
45        prefix: ([`str`]) prefix to add before each line item.
46        format_value: optional callable to convert each value to a string.
47            If not specified, then an appropriate converter will be inferred
48            based on the values. If specified, then the callable must accept
49            1 positional arg and return a string.
50
51    Returns:
52        [`str`]; the values formatted as a human-friendly list.
53    """
54    if not values:
55        return "{}<empty>".format(prefix)
56
57    if format_value == None:
58        format_value = guess_format_value(values)
59
60    # Subtract 1 because we start at 0; i.e. length 10 prints 0 to 9
61    max_i_width = len(str(len(values) - 1))
62
63    return "\n".join([
64        "{prefix}{ipad}{i}: {value}".format(
65            prefix = prefix,
66            ipad = " " * (max_i_width - len(str(i))),
67            i = i,
68            value = format_value(v),
69        )
70        for i, v in enumerate(values)
71    ])
72
73def guess_format_value(values):
74    """Guess an appropriate human-friendly formatter to use with the value.
75
76    Args:
77        values: The object to pick a formatter for.
78
79    Returns:
80        callable that accepts the value.
81    """
82    found_types = {}
83    for value in values:
84        found_types[type(value)] = None
85        if len(found_types) > 1:
86            return repr_with_type
87    found_types = found_types.keys()
88    if len(found_types) != 1:
89        return repr_with_type
90    elif found_types[0] in ("string", "File"):
91        # For strings: omit the extra quotes and escaping. Just noise.
92        # For Files: they include <TYPE path> already
93        return _informative_str
94    else:
95        return repr_with_type
96
97def maybe_sorted(container, allow_sorting = True):
98    """Attempts to return the values of `container` in sorted order, if possible.
99
100    Args:
101        container: ([`list`] | (or other object convertible to list))
102        allow_sorting: ([`bool`]) whether to sort even if it can be sorted. This
103            is primarily so that callers can avoid boilerplate when they have
104            a "should it be sorted" arg, but also always convert to a list.
105
106    Returns:
107        A list, in sorted order if possible, otherwise in the original order.
108        This *may* be the same object as given as input.
109    """
110    container = to_list(container)
111    if not allow_sorting:
112        return container
113
114    if all([_is_sortable(v) for v in container]):
115        return sorted(container)
116    else:
117        return container
118
119def _is_sortable(obj):
120    return (
121        types.is_string(obj) or types.is_int(obj) or types.is_none(obj) or
122        types.is_bool(obj)
123    )
124
125def to_list(obj):
126    """Attempt to convert the object to a list, else error.
127
128    NOTE: This only supports objects that are typically understood as
129    lists, not any iterable. Types like `dict` and `str` are iterable,
130    but will be rejected.
131
132    Args:
133        obj: ([`list`] | [`depset`]) The object to convert to a list.
134
135    Returns:
136        [`list`] of the object
137    """
138    if types.is_string(obj):
139        fail("Cannot pass string to to_list(): {}".format(obj))
140    elif types.is_list(obj):
141        return obj
142    elif types.is_depset(obj):
143        return obj.to_list()
144    else:
145        fail("Unable to convert to list: {}".format(repr_with_type(obj)))
146