HypothesisWorks / hypothesis

Hypothesis is a powerful, flexible, and easy to use library for property-based testing.
https://hypothesis.works
Other
7.57k stars 587 forks source link

Improve discrimination of database keys #3867

Open jobh opened 9 months ago

jobh commented 9 months ago

We don't (and can't) guarantee that our database keys are unique. If two test methods have identical keys, the consequences are limited: We replay arbitrary examples instead of previously-failing ones. This is a low-priority issue, but recorded here as a reminder.

While the downsides are limited, they do exist: a slight slowdown plus loss of the database benefits. Hence, we should try hard (within reason) to actually generate unique-yet-stable database keys.

Some ideas are:

One common failure mode in our own tests is inner tests used in multiple contexts, either by parametrized strategies or inside a helper function (tests.common.debug).

Zac-HD commented 9 months ago

I agree that we should pick up any low-hanging improvements here, and this list looks pretty good to me.

Specific to the last point, we have a magic attribute used to distinguish @pytest.mark.parametrize()d tests, which could also be used here. Just assign whatever bytestring you like, and it'll be hashed into the database key along with everything else, e.g.: https://github.com/HypothesisWorks/hypothesis/blob/7b634834b1dfa00674d1b614a7bb857017bf8d7c/hypothesis-python/src/_hypothesis_pytestplugin.py#L297-L299

jobh commented 9 months ago

Hmm, I think we might add to the list

otherwise we'd duplicate keys for f.x.

@pytest.mark.parametrize("v", [1, 2])
def test_something(v):

    @given(st.integers(v))
    def inner(x):
        assert x >= v

    inner()

since the attribute is added to test_something and not inner. At that point, it might be more straightforward to just store the add_digest as a global/threadlocal rather than as an attribute.

[edit] ...actually, we could mix in currently executing nodeid regardless of parametrization... and call it a day. Too pytest specific?

Zac-HD commented 9 months ago

[edit] ...actually, we could mix in currently executing nodeid regardless of parametrization... and call it a day. Too pytest specific?

Too specific - the problem is that if you execute a particular test function manually or via pytest, or a method with unittest or pytest, we want to have the same database key in either case. So mixing in the nodeid is fine when it's a parametrized test because there's a solid benefit and we're unlikely to execute it without pytest, but I'd prefer to avoid doing that unconditionally.

jobh commented 9 months ago

Understood, thanks! I have just one more question:

We don't use the strategy definition in our db key, and it looks intentional (e.g., _clean_source in get_digest and ignoring specifier in find()). This leads to collisions in find (naturally), and also f.x. test_attrs_inference_builds which is defined by the same name but slightly different strategy in two test modules.

Why is this? Is it beneficial to have key stability across changes in strategy?

Zac-HD commented 9 months ago

It's actually pretty hard to derive a stable-across-runs bytestring from an arbitrary strategy; for example it's easy to end up with a repr that includes some object's memory address, or less often something like the current time or date or machine name or OS.

We strip the decorators from source code before hashing it so that adding or removing @settings() and @example() decorators doesn't affect the database key.