Skip to content

Hypothesis testing #136

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

Merged
merged 10 commits into from
Jul 4, 2022
Merged

Hypothesis testing #136

merged 10 commits into from
Jul 4, 2022

Conversation

samuelcolvin
Copy link
Member

@Zac-HD hope you don't mind me tagging you in this and asking for advice, let me know if you'd prefer me to create an issue on hypothesis.

There's a particular failure I'd like hypothesis to find so I can be confident my fix (#134) for it works generally.

The example is demonstrated in test_recursive_broken - basically recursive data with a cyclic reference. Currently it seems hypothesis is not generating this case. Is there a way to get it to do so?

@codecov
Copy link

codecov bot commented Jun 29, 2022

Codecov Report

Merging #136 (3c6f2de) into main (d3a6bf3) will decrease coverage by 0.01%.
The diff coverage is n/a.

❗ Current head 3c6f2de differs from pull request most recent head 3e1e69e. Consider uploading reports for the commit 3e1e69e to get more accurate results

@@            Coverage Diff             @@
##             main     #136      +/-   ##
==========================================
- Coverage   97.45%   97.44%   -0.02%     
==========================================
  Files          41       41              
  Lines        4007     4068      +61     
  Branches       28       28              
==========================================
+ Hits         3905     3964      +59     
- Misses        102      104       +2     
Impacted Files Coverage Δ
src/errors/validation_exception.rs 95.45% <0.00%> (-0.70%) ⬇️
src/validators/literal.rs 96.05% <0.00%> (-0.10%) ⬇️
src/validators/model_class.rs 96.90% <0.00%> (-0.04%) ⬇️
src/validators/function.rs 94.97% <0.00%> (-0.03%) ⬇️
src/validators/typed_dict.rs 99.10% <0.00%> (-0.02%) ⬇️
src/input/datetime.rs 97.97% <0.00%> (-0.02%) ⬇️
src/errors/mod.rs 92.30% <0.00%> (ø)
src/build_tools.rs 98.43% <0.00%> (ø)
src/errors/kinds.rs 100.00% <0.00%> (ø)
src/input/shared.rs 100.00% <0.00%> (ø)
... and 19 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9899af5...3e1e69e. Read the comment docs.

Comment on lines 72 to 79
class BranchModel(TypedDict):
name: str
sub_branch: Optional['BranchModel']


@settings(**settings_dict)
@given(strategies.from_type(BranchModel))
def test_recursive_hyp(recursive_schema, data):
Copy link

@Zac-HD Zac-HD Jun 29, 2022

Choose a reason for hiding this comment

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

The reason that this test passes is that Hypothesis doesn't generate cyclic references by default; unless you write a strategy which can explicitly choose between "generate a new BranchModel" and "reuse an existing BranchModel" you'll always get the former. That might look something like:

@st.composite
def branch_models_with_cycles(draw, existing: list[BranchModel] | None = None) -> BranchModel:
    if existing is None:
        existing = []
    name = draw(st.text())
    sub_branch = draw(st.builds(BranchModel) | st.sampled_from(existing))  # might need to handle empty-list case?
    existing.append(sub_branch)  # ok this recursion scheme is broken but you get the idea
    return BranchModel(name=name, sub_branch=sub_branch)

Copy link
Member Author

Choose a reason for hiding this comment

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

Looks good, I'll add this.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've tried tweaking this and got a recursion error in hypothesis (I think)

Adjusted function:

@strategies.composite
def branch_models_with_cycles(draw, existing: list[BranchModel] = strategies.from_type(list[BranchModel])):
    # debug(existing)
    name = draw(strategies.text())
    sub_branch = draw(strategies.from_type(BranchModel) | strategies.sampled_from(existing))
    branch = BranchModel(name=name, sub_branch=sub_branch)
    existing.append(branch)
    return branch

The traceback is massive, gist of the output here.

Copy link

Choose a reason for hiding this comment

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

This one works:

@st.composite
def branch_models_with_cycles(draw, existing: list[BranchModel] | None = None):
    if existing is None:
        existing = []
    name = draw(st.text())
    sub_branch = draw(
        st.none()
        | (st.sampled_from(existing) if existing else st.nothing())
        | branch_models_with_cycles(existing)
    )
    branch = BranchModel(name=name, sub_branch=sub_branch)
    existing.append(branch)
    return branch

I think your underlying problem was that existing: list[BranchModel] = strategies.from_type(list[BranchModel]) is ill-typed; the default value is a strategy which generates list[BranchModel], rather than an instance of that type.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've tried that unfortunately it's not causing a failure, since the base of this PR pre-dates #134, I think that means this code still isn't generate cyclic references.

(Ignore the failures on CI, that's just rust nightly and will fix itself soon).

Copy link

Choose a reason for hiding this comment

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

🤦 yeah, by the clear light of day we have to append to the existing list before we draw from it, not afterwards, and then mutate the sub_branch in place afterwards:

@st.composite
def branch_models_with_cycles(draw, existing=None):
    if existing is None:
        existing = []
    model = BranchModel(name=draw(st.text()), sub_branch=None)
    existing.append(model)
    model["sub_branch"] = draw(
        st.none()
        | st.builds(BranchModel, name=st.text(), sub_branch=branch_models_with_cycles(existing))
        | st.sampled_from(existing)
    )
    return model

I've checked that this generates cycles as well as just generating values at all. Sorry for the back-and-forth!

Copy link
Member Author

Choose a reason for hiding this comment

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

Not at all! I should be the one apologising.

Thanks so much, that's working now.

@Zac-HD
Copy link

Zac-HD commented Jun 29, 2022

Don't actually have a working demo, but here's the jotted-off-in-five-minutes version; I can help with a real fix on the weekend if you need it 🙂

@samuelcolvin samuelcolvin marked this pull request as ready for review July 4, 2022 05:39
@samuelcolvin samuelcolvin merged commit 9b4c074 into main Jul 4, 2022
@samuelcolvin samuelcolvin deleted the hypothesis-testing branch July 4, 2022 05:39
@samuelcolvin
Copy link
Member Author

@Zac-HD thanks so much for your help on this.

I'll extend the hypothesis tests more in future.

@Zac-HD
Copy link

Zac-HD commented Jul 4, 2022

Very happy to help 😁

If there's ever a way I can help give Pydantic first-class support for Hypothesis, just let me know!

@samuelcolvin
Copy link
Member Author

samuelcolvin commented Jul 4, 2022

Thanks so much for the offer, I'll let you know when we get to that point.

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.

2 participants