-
Notifications
You must be signed in to change notification settings - Fork 157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hypothesis h2 settings equality #543
Hypothesis h2 settings equality #543
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -333,7 +333,7 @@ def test_cannot_set_invalid_values_for_max_frame_size(self, val): | |||
assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR | |||
assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16384 | |||
|
|||
@given(integers()) | |||
@given(strategies.integers()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reasoning for this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it matters. 😉 Just curious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very true. I'll play around with this some more and see what advice @alexwlchan has.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You’ve got MAX_FRAME_SIZE
twice.
test/test_settings.py
Outdated
@@ -333,7 +333,7 @@ def test_cannot_set_invalid_values_for_max_frame_size(self, val): | |||
assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR | |||
assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16384 | |||
|
|||
@given(integers()) | |||
@given(strategies.integers()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, a minor thing: can we pick variable names that are a bit more descriptive please? 😄
Believe all your comments are addressed now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
False
My 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How embarrassing 😁 . Yes definitely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -111,4 +111,5 @@ In chronological order: | |||
- Fred Thomsen (@fredthomsen) | |||
|
|||
- Added logging. | |||
|
|||
- Enchance equality testing of ``h2.settings.Settings`` objects with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/s/Enchance/Enhance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still probably want to deal with this. 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, this looks great! Thanks!
🎉 |
WIP for equality and property test using hypothesis to address #466.