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

[red-knot] Property tests #14178

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

[red-knot] Property tests #14178

wants to merge 2 commits into from

Conversation

sharkdp
Copy link
Contributor

@sharkdp sharkdp commented Nov 7, 2024

Summary

This PR adds a new property_tests module with quickcheck-based tests that verify certain properties of types. The following properties are currently checked:

  • is_equivalent_to:
    • is reflexive: T is equivalent to itself
  • is_subtype_of:
    • is reflexive: T is a subtype of T
    • is antisymmetric: if S <: T and T <: S, then S is equivalent to T
    • is transitive: S <: T & T <: U => S <: U
  • is_disjoint_from:
    • is irreflexive: T is not disjoint from T
    • is symmetric: S disjoint from T => T disjoint from S
  • is_assignable_to:
    • is reflexive
  • negate:
    • is an involution: T.negate().negate() is equivalent to T

There are also some tests that validate higher-level properties like:

  • S <: T implies that S is not disjoint from T
  • S <: T implies that S is assignable to T
  • A singleton type must also be single-valued

These tests found a few bugs so far:

Some additional notes:

  • Quickcheck-based property tests are non-deterministic and finding counter-examples might take an arbitrary long time. This makes them bad candidates for running in CI (for every PR). We can think of running them in a cron-job way from time to time, similar to fuzzing. But for now, it's only possible to run them locally (see instructions in source code).
  • Some tests currently find false positive "counterexamples" because our understanding of equivalence of types is not yet complete. We do not understand that int | str is the same as str | int, for example.
  • Properties can not be formulated in every way possible, due to the fact that is_disjoint_from and is_subtype_of can produce false negative answers.
  • The current shrinking implementation is very naive, which leads to counterexamples that are very long (str & Any & ~tuple[Any] & ~tuple[Unknown] & ~Literal[""] & ~Literal["a"] | str & int & ~tuple[Any] & ~tuple[Unknown]), requiring the developer to simplify manually. It has not been a major issue so far, but there is a comment in the code how this can be improved.
  • The tests are currently implemented using a macro. This is a single commit on top which can easily be reverted, if we prefer the plain code instead. With the macro:
    // `S <: T` implies that `S` can be assigned to `T`.
    type_property_test!(
        subtype_of_implies_assignable_to, db, (s, t),
        s.is_subtype_of(db, t) => s.is_assignable_to(db, t)
    );
    without the macro:
    /// `S <: T` implies that `S` can be assigned to `T`.
    #[quickcheck]
    fn subtype_of_implies_assignable_to(s: Ty, t: Ty) -> bool {
        let db = get_cached_db();
    
        let s = s.into_type(&db);
        let t = t.into_type(&db);
    
        !s.is_subtype_of(&*db, t) || s.is_assignable_to(&*db, t)
    }

Test Plan

while cargo test --release -p red_knot_python_semantic --features property_tests types::property_tests; do :; done

@AlexWaygood AlexWaygood added the red-knot Multi-file analysis & type inference label Nov 7, 2024
@sharkdp sharkdp force-pushed the david/property-tests branch 2 times, most recently from c1f05ab to 634b4f6 Compare November 7, 2024 22:17
Comment on lines 3325 to 3916
let db = setup_db();

let t1 = t1.into_type(&db);
let t2 = t2.into_type(&db);
let t3 = t3.into_type(&db);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we want to move forward with this, we could probably auto-generate this boilerplate in each test using a macro.

sharkdp added a commit that referenced this pull request Nov 7, 2024
## Summary

Minor fix to `Type::is_subtype_of` to make sure that Boolean literals
are subtypes of `int`, to match runtime semantics.

Found this while doing some property-testing experiments [1].

[1] #14178

## Test Plan

New unit test.
Copy link
Contributor

github-actions bot commented Nov 7, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

sharkdp added a commit that referenced this pull request Nov 8, 2024
)

## Summary

Another bug found using [property
testing](#14178).

## Test Plan

New unit test
@carljm
Copy link
Contributor

carljm commented Nov 8, 2024

This is awesome! Thank you for doing this, I've been wanting to explore this. I think property testing is very well suited to testing type relation invariants, and I do think we should move forward with actually landing this.

@sharkdp sharkdp force-pushed the david/property-tests branch 3 times, most recently from 40de0e3 to 207eb44 Compare December 2, 2024 14:45
Comment on lines +52 to +53
quickcheck = { version = "1.0.3" }
quickcheck_macros = { version = "1.0.0" }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dev-dependencies can't be optional, otherwise I would have made these dependent on the property_tests feature.

@sharkdp sharkdp marked this pull request as ready for review December 2, 2024 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
red-knot Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants