bazelbuild / rules_testing

Starlark testing framework and utility libraries
https://rules-testing.readthedocs.io/
Apache License 2.0
22 stars 5 forks source link

Support more complex types by using generics #93

Open matts1 opened 8 months ago

matts1 commented 8 months ago

It's currently very hard to work with custom providers. One example of somewhere seriously lacking is if I want to do assertions on the following type:

FooInfo = provider(fields = {"name": "str")
BarInfo = provider(fields = {
    "foos": "(depset[Foo]) the foos",
})

I would like to be able to write something like:

env.expect.that_provider(BarInfo).foos()
  .contains_at_least_predicates(lambda foo: foo.name().equals("foo"))

I propose that we allow generics by turning generic types into functions. For example:

FooInfoSubject = subjects.struct(name = subjects.string)
BarInfoSubject = subjects.struct(
    foos = subjects.depset(FooInfoSubject)
)

This would make the above predicates far more easily implemented (although the caveat is that it would require a change in matchers so that you could simply use them as a boolean predicate - at the moment, not matching simply fails the test).

We could also use this to easily solve #63 via the generic type subjects.optional (eg. subjects.optional(subjects.string)).

matts1 commented 8 months ago

FWIW, I've mostly solved this by just doing the following:

# I strongly dislike how struct is defined, IMO the attrs should be curried
def struct(**attrs):
    return lambda value, *, meta: subjects.struct(value, meta=meta, attrs=attrs)

def optional(factory):
    def new_factory(value, *, meta):
        # Optimisations could be done here to just pull all the methods from the parent and add the method is_none()
        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)

generic_subjects = struct(struct = struct, optional = optional)

You can then just write:

FooInfo = provider(fields = {"a": "(Optional[str])")
FooInfoSubject = generic_subjects.struct(a = generic_subjects.optional(subjects.str))

foo_some = FooInfo(a = "abc")
foo_none = FooInfo(a = None)

env.expect.that_value(foo_some, factory = FooInfoSubject).a().some().contains("abc")
env.expect.that_value(foo_none, factory = FooInfoSubject).a().is_none()