Hypothesis h2 settings equality#543
Hypothesis h2 settings equality#543Lukasa merged 2 commits intopython-hyper:masterfrom fredthomsen:HypothesisSettingsEquality
Conversation
Lukasa
left a comment
There was a problem hiding this comment.
Cool, so this is a really good start! I'm going to ask @alexwlchan to do the majority of the reviewing here because he's the best placed to do it.
test/test_settings.py
Outdated
| assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16384 | ||
|
|
||
| @given(integers()) | ||
| @given(strategies.integers()) |
There was a problem hiding this comment.
What's the reasoning for this change?
There was a problem hiding this comment.
I did this since I was using more functions within the strategies submodule along with integers. Probably just a nervous tick I have 😁 . I am happy to conform to whatever style you deem appropriate ie change this back and just import all the functions from the strategies submodule that I use.
There was a problem hiding this comment.
I don't think it matters. 😉 Just curious.
There was a problem hiding this comment.
It makes no difference, so I’d prefer to change it back to keep the diff focused. 🙂
test/test_settings.py
Outdated
| h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE, | ||
| ]), | ||
| values=strategies.integers(min_value=0)) | ||
| ) |
There was a problem hiding this comment.
So this strategy generally seems ok, except that it'll frequently generate invalid initial settings that the settings object will reject. We'll need to expand this out to ensure that we only generate valid initial settings (or that we discard any attempt to create an invalid one).
There was a problem hiding this comment.
Yup. Hadn't gotten there yet, but this is next.
test/test_settings.py
Outdated
| assert not (a != b) | ||
|
|
||
| def test_different_eq(self): | ||
| @given(a=SettingsStrategy, b=SettingsStrategy) |
There was a problem hiding this comment.
So it's entirely possible that this would generate two settings objects that are identical. We need to be a bit careful here and construct a test that allows us to ensure that the two objects are different. In this case, the easiest way might be to create two strategies that cannot generate overlapping Settings objects in some way, or at least create a Settings object that our strategy cannot generate. Of course, if @alexwlchan has more useful things to suggest here that'd be really helpful (he's our resident Hypothesis expert).
There was a problem hiding this comment.
Very true. I'll play around with this some more and see what advice @alexwlchan has.
There was a problem hiding this comment.
I’m not sure Hypothesis has an easy way to generate two objects that are guaranteed to be distinct – any way we do that would rely on the equality operators, which are precisely what we’re trying to test!
Something like:
from hypothesis import assume
@given(SettingsStrategy, SettingsStrategy)
def test_equality_operators(x, y):
assume(x != y):
assert not (x == y)Here assume will cause Hypothesis to discard this example, and try to pick different examples as part of the quota. But it relies on the operators we’re testing – a bit icky.
|
ACK: I’ve seen this and will review it, but I probably won’t have time for a proper review until later this week. |
|
❤️ Thanks @alexwlchan. |
alexwlchan
left a comment
There was a problem hiding this comment.
Okay, looks good so far! I’d like to see the strategy tweaked to produce only valid settings objects, but it seems like a sensible approach.
I’m not convinced adapting the instance() and anotherInstance() methods are the right way to go here – producing two objects that are guaranteed distinct will be tricky, particularly because Hypothesis’s shrinking will tend them both to the same point if there’s a failure.
One way I’ve done equality testing in the past is to switch what the test does based on whether we have equal or distinct objects. Something like:
@given(SettingsStrategy)
def test_equality(x):
assert x == x
assert not (x != x)
@given(SettingsStrategy, SettingsStrategy)
def test_equality_operators(x, y):
if x == y:
assert x == y
assert not (x != y)
else:
assert x != y
assert not (x == y)The second test usually produces two objects that are unequal, so we have the first test to counter it out. Not quite ideal, but it’s worked pretty well in the past.
test/test_settings.py
Outdated
| h2.settings.SettingCodes.INITIAL_WINDOW_SIZE, | ||
| h2.settings.SettingCodes.MAX_FRAME_SIZE, | ||
| h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS, | ||
| h2.settings.SettingCodes.MAX_FRAME_SIZE, |
There was a problem hiding this comment.
You’ve got MAX_FRAME_SIZE twice.
test/test_settings.py
Outdated
| assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16384 | ||
|
|
||
| @given(integers()) | ||
| @given(strategies.integers()) |
There was a problem hiding this comment.
It makes no difference, so I’d prefer to change it back to keep the diff focused. 🙂
test/test_settings.py
Outdated
| assert not (a != b) | ||
|
|
||
| def test_different_eq(self): | ||
| @given(a=SettingsStrategy, b=SettingsStrategy) |
There was a problem hiding this comment.
I’m not sure Hypothesis has an easy way to generate two objects that are guaranteed to be distinct – any way we do that would rely on the equality operators, which are precisely what we’re trying to test!
Something like:
from hypothesis import assume
@given(SettingsStrategy, SettingsStrategy)
def test_equality_operators(x, y):
assume(x != y):
assert not (x == y)Here assume will cause Hypothesis to discard this example, and try to pick different examples as part of the quota. But it relies on the operators we’re testing – a bit icky.
test/test_settings.py
Outdated
| ) | ||
|
|
||
| @given(o=SettingsStrategy) | ||
| def test_identical_eq(self, o): |
There was a problem hiding this comment.
Also, a minor thing: can we pick variable names that are a bit more descriptive please? 😄
|
Believe all your comments are addressed now. |
alexwlchan
left a comment
There was a problem hiding this comment.
Thanks, this is looking pretty good now.
I realise I misled you somewhat with a previous comment – apologies. Hopefully just a minor fix, not a major change. Otherwise a few minor things, but this feels pretty close to done now.
test/test_settings.py
Outdated
| h2.settings.Settings, | ||
| client=booleans(), | ||
| initial_values=fixed_dictionaries({ | ||
| h2.settings.SettingCodes.HEADER_TABLE_SIZE: integers(0), |
There was a problem hiding this comment.
Hypothesis also has a just() strategy, which returns a single value every time. Mildly more efficient, IIRC, although I don't think it makes a massive difference.
There was a problem hiding this comment.
Not sure I understand where you want to apply the just strategy here. If you are referring to this specific line, the value listed here is just the minimum value for the integers strategy.
There was a problem hiding this comment.
Oh yeah, oops, I was misreading integers(0) as integers(min_size=0, max_size=0).
Ignore me.
test/test_settings.py
Outdated
| h2.settings.SettingCodes.ENABLE_PUSH: integers(0, 1), | ||
| h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: | ||
| integers(0, 2**31 - 1), | ||
| h2.settings.SettingCodes.MAX_FRAME_SIZE: integers(16384, 16777215), |
There was a problem hiding this comment.
Maybe replace these two with the constants they're derived from? i.e.
h2.settings.SettingCodes.MAX_FRAME_SIZE: integers(2**14, 2**24 - 1)|
|
||
| def test_different_ne(self): | ||
| @given(settings=SettingsStrategy, o_settings=SettingsStrategy) | ||
| def test_equality_multiple(self, settings, o_settings): |
There was a problem hiding this comment.
Maybe other_settings? We’re not tight for characters.
| b = self.another_instance() | ||
| assert not (a == b) | ||
| assert (settings == settings) | ||
| assert not (settings != settings) |
There was a problem hiding this comment.
I’ve realised since writing my last comment that this isn’t always the best test, because you’re comparing the identity of two identical objects. Python will default to using identity as an equality test, so this passes even if you don't supply custom equality methods:
>>> class Foo: pass
...
>>> x = Foo()
>>> y = Foo()
>>> x == x
True
>>> x == y
False
>>> x != x
FalseMy preferred approach is now to test based on copies of objects, i.e.,
other_settings = copy.deepcopy(settings)
assert (settings == other_settings)
assert not (settings != other_settings)although this does rely on you being able to copy your objects, and I can’t remember what the options are here for IntEnum and its subclasses.
There was a problem hiding this comment.
Ok, then it seems like this test makes little sense then as we have another where two strategies are generated, so I'll just nix this one.
| assert (a != b) == [b] | ||
| delg = Delegate() | ||
| assert (settings == delg) == [delg] | ||
| assert (settings != delg) == [delg] |
There was a problem hiding this comment.
I realise we nicked this test from elsewhere, but would it be better to tweak the return values from __eq__ and __ne__ so we can be sure we're really calling the right method?
e.g.
def __eq__(self, other):
return [self]
def __ne__(self, other):
return [self, self]There was a problem hiding this comment.
How embarrassing 😁 . Yes definitely.
There was a problem hiding this comment.
Why is this worthy of it's own test btw? Can you enlighten me how this is not a given?
There was a problem hiding this comment.
Err…
This comes from the PyOpenSSL test suite, a module which used to have a lot more C in its core lib than Python, so these things weren’t generally a given. I’m not sure it’s as useful for hyper-h2, and wouldn’t object if you chose to remove it.
CONTRIBUTORS.rst
Outdated
|
|
||
| - Added logging. | ||
|
|
||
| - Enchance equality testing of ``h2.settings.Settings`` objects with |
There was a problem hiding this comment.
Still probably want to deal with this. 😁
alexwlchan
left a comment
There was a problem hiding this comment.
Wasn't there a test that asserted an object (or its copy) was equal to itself? I think that's still a useful test to have – in practice, the current test_equality will almost always go down the not-equal branch.
test/test_settings.py
Outdated
| h2.settings.Settings, | ||
| client=booleans(), | ||
| initial_values=fixed_dictionaries({ | ||
| h2.settings.SettingCodes.HEADER_TABLE_SIZE: integers(0), |
There was a problem hiding this comment.
Oh yeah, oops, I was misreading integers(0) as integers(min_size=0, max_size=0).
Ignore me.
|
@alexwlchan Yes, and to your point I restored that test. As you said, it will rarely go down that path of being equal when there are multiple settings generated from the strategy. |
alexwlchan
left a comment
There was a problem hiding this comment.
Sorry for the somewhat slow reviews on this, but this looks really good now, thanks!
@Lukasa I don’t think I have any other comments on this patch – happy to merge if you are.
😄
|
Cool, shall we bring it up to date with the current master first? |
|
Branch has been rebased onto latest master. |
Lukasa
left a comment
There was a problem hiding this comment.
One very small note, and then this looks ok!
test/test_settings.py
Outdated
| h2.settings.SettingCodes.MAX_FRAME_SIZE: | ||
| integers(2**14, 2**24 - 1), | ||
| h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: integers(0), | ||
| h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: integers(0), |
There was a problem hiding this comment.
Are we worried about creating values outside the valid range of some of these settings? These settings do have upper limits on their valid values.
There was a problem hiding this comment.
Yes. I looked at the specs and saw that these are 32 bit fields and thus have adjusted them accordingly.
In this case we should update the _validate_settings function as well.
|
Fixed misspelling in contributors file. I had made that change earlier, but I must have inadvertently blown it away. |
Lukasa
left a comment
There was a problem hiding this comment.
Alright, this looks great! Thanks!
|
🎉 |
WIP for equality and property test using hypothesis to address #466.