Skip to content
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

Merged
merged 2 commits into from
May 30, 2017
Merged

Hypothesis h2 settings equality #543

merged 2 commits into from
May 30, 2017

Conversation

fredthomsen
Copy link
Contributor

WIP for equality and property test using hypothesis to address #466.

Copy link
Member

@Lukasa Lukasa left a 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.

@@ -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())
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

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. 🙂

h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE,
]),
values=strategies.integers(min_value=0))
)
Copy link
Member

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).

Copy link
Contributor Author

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.

assert not (a != b)

def test_different_eq(self):
@given(a=SettingsStrategy, b=SettingsStrategy)
Copy link
Member

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).

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@alexwlchan
Copy link
Contributor

ACK: I’ve seen this and will review it, but I probably won’t have time for a proper review until later this week.

@Lukasa
Copy link
Member

Lukasa commented Apr 10, 2017

❤️ Thanks @alexwlchan.

Copy link
Contributor

@alexwlchan alexwlchan left a 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.

h2.settings.SettingCodes.INITIAL_WINDOW_SIZE,
h2.settings.SettingCodes.MAX_FRAME_SIZE,
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS,
h2.settings.SettingCodes.MAX_FRAME_SIZE,
Copy link
Contributor

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.

@@ -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())
Copy link
Contributor

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. 🙂

assert not (a != b)

def test_different_eq(self):
@given(a=SettingsStrategy, b=SettingsStrategy)
Copy link
Contributor

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.

)

@given(o=SettingsStrategy)
def test_identical_eq(self, o):
Copy link
Contributor

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? 😄

@fredthomsen
Copy link
Contributor Author

Believe all your comments are addressed now.

Copy link
Contributor

@alexwlchan alexwlchan left a 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.

h2.settings.Settings,
client=booleans(),
initial_values=fixed_dictionaries({
h2.settings.SettingCodes.HEADER_TABLE_SIZE: integers(0),
Copy link
Contributor

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.

Copy link
Contributor Author

@fredthomsen fredthomsen May 15, 2017

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.

Copy link
Contributor

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.

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),
Copy link
Contributor

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):
Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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]
Copy link
Contributor

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]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How embarrassing 😁 . Yes definitely.

Copy link
Contributor Author

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?

Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/s/Enchance/Enhance

Copy link
Member

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. 😁

@fredthomsen fredthomsen changed the title WIP: Hypothesis h2 settings equality Hypothesis h2 settings equality May 15, 2017
Copy link
Contributor

@alexwlchan alexwlchan left a 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.

h2.settings.Settings,
client=booleans(),
initial_values=fixed_dictionaries({
h2.settings.SettingCodes.HEADER_TABLE_SIZE: integers(0),
Copy link
Contributor

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.

@fredthomsen
Copy link
Contributor Author

@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.

Copy link
Contributor

@alexwlchan alexwlchan left a 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.

😄

@Lukasa
Copy link
Member

Lukasa commented May 16, 2017

Cool, shall we bring it up to date with the current master first?

@fredthomsen
Copy link
Contributor Author

Branch has been rebased onto latest master.

Copy link
Member

@Lukasa Lukasa left a 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!

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),
Copy link
Member

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.

Copy link
Contributor Author

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.

@fredthomsen
Copy link
Contributor Author

Fixed misspelling in contributors file. I had made that change earlier, but I must have inadvertently blown it away.

Copy link
Member

@Lukasa Lukasa left a 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!

@Lukasa Lukasa merged commit e0adb86 into python-hyper:master May 30, 2017
@alexwlchan
Copy link
Contributor

🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants