gpui: Add `property_test` macro (#50935)

Cameron Mcloughlin and Conrad Irwin created

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

Cargo.lock                                      | 283 ++++++++++++------
Cargo.toml                                      |   3 
crates/editor/Cargo.toml                        |   5 
crates/editor/src/editor_tests.rs               |   3 
crates/editor/src/editor_tests/property_test.rs |  85 +++++
crates/gpui/Cargo.toml                          |   2 
crates/gpui/src/gpui.rs                         |   7 
crates/gpui/src/test.rs                         |  33 ++
crates/gpui_macros/Cargo.toml                   |   2 
crates/gpui_macros/src/gpui_macros.rs           |  74 ++++
crates/gpui_macros/src/property_test.rs         | 199 +++++++++++++
crates/sum_tree/Cargo.toml                      |   6 
crates/sum_tree/src/property_test.rs            |  32 ++
crates/sum_tree/src/sum_tree.rs                 |   2 
crates/text/Cargo.toml                          |   1 
15 files changed, 641 insertions(+), 96 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -731,7 +731,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -1128,7 +1128,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -1196,7 +1196,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -1226,7 +1226,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2065,7 +2065,7 @@ dependencies = [
  "regex",
  "rustc-hash 2.1.1",
  "shlex",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2083,7 +2083,7 @@ dependencies = [
  "regex",
  "rustc-hash 2.1.1",
  "shlex",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2218,7 +2218,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2247,7 +2247,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2397,7 +2397,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2482,7 +2482,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -2736,7 +2736,7 @@ dependencies = [
  "quote",
  "serde",
  "serde_json",
- "syn 2.0.106",
+ "syn 2.0.117",
  "tempfile",
  "toml 0.8.23",
 ]
@@ -2965,7 +2965,7 @@ dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -3643,6 +3643,15 @@ dependencies = [
  "unicode-segmentation",
 ]
 
+[[package]]
+name = "convert_case"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "copilot"
 version = "0.1.0"
@@ -4354,7 +4363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
 dependencies = [
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4431,7 +4440,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "scratch",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4445,7 +4454,7 @@ dependencies = [
  "indexmap",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4463,7 +4472,7 @@ dependencies = [
  "indexmap",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4560,7 +4569,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strsim",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4571,7 +4580,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
 dependencies = [
  "darling_core",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4803,7 +4812,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4825,7 +4834,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustc_version",
- "syn 2.0.106",
+ "syn 2.0.117",
  "unicode-xid",
 ]
 
@@ -4835,7 +4844,7 @@ version = "0.1.0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -4847,7 +4856,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -5038,7 +5047,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -5101,7 +5110,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strum 0.27.2",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -5476,6 +5485,8 @@ dependencies = [
  "parking_lot",
  "pretty_assertions",
  "project",
+ "proptest",
+ "proptest-derive",
  "rand 0.9.2",
  "regex",
  "release_channel",
@@ -5652,7 +5663,7 @@ dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -5673,7 +5684,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -5738,7 +5749,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -6213,7 +6224,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -6512,7 +6523,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -6781,7 +6792,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -7192,7 +7203,7 @@ source = "git+https://github.com/zed-industries/gh-workflow?rev=c9eac0ed361583e1
 dependencies = [
  "heck 0.5.0",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -7441,7 +7452,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -7656,6 +7667,7 @@ dependencies = [
  "postage",
  "pretty_assertions",
  "profiling",
+ "proptest",
  "rand 0.9.2",
  "raw-window-handle",
  "refineable",
@@ -7783,7 +7795,7 @@ dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -8211,7 +8223,7 @@ dependencies = [
  "markup5ever 0.12.1",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -8669,7 +8681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
 dependencies = [
  "byteorder-lite",
- "quick-error",
+ "quick-error 2.0.1",
 ]
 
 [[package]]
@@ -8745,7 +8757,7 @@ checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -8840,7 +8852,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -9032,7 +9044,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -10303,7 +10315,7 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -11170,7 +11182,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -11253,7 +11265,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -11646,7 +11658,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -11675,7 +11687,7 @@ checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -11733,7 +11745,7 @@ dependencies = [
  "proc-macro2",
  "proc-macro2-diagnostics",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -11843,7 +11855,7 @@ dependencies = [
  "by_address",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -12106,7 +12118,7 @@ dependencies = [
  "pest_meta",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -12585,7 +12597,7 @@ dependencies = [
  "phf_shared 0.11.3",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -12598,7 +12610,7 @@ dependencies = [
  "phf_shared 0.12.1",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -12660,7 +12672,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -12984,7 +12996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
 dependencies = [
  "proc-macro2",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -13048,7 +13060,7 @@ dependencies = [
  "proc-macro-error-attr2",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -13068,7 +13080,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
  "version_check",
  "yansi",
 ]
@@ -13099,7 +13111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
 dependencies = [
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -13308,6 +13320,47 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "proptest"
+version = "1.10.0"
+source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b#3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b"
+dependencies = [
+ "bit-set",
+ "bit-vec",
+ "bitflags 2.10.0",
+ "num-traits",
+ "proptest-macro",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
+ "rand_xorshift",
+ "regex-syntax",
+ "rusty-fork",
+ "tempfile",
+ "unarray",
+]
+
+[[package]]
+name = "proptest-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c57924a81864dddafba92e1bf92f9bf82f97096c44489548a60e888e1547549b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "proptest-macro"
+version = "0.5.0"
+source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b#3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b"
+dependencies = [
+ "convert_case 0.11.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
 [[package]]
 name = "prost"
 version = "0.9.0"
@@ -13365,7 +13418,7 @@ dependencies = [
  "prost 0.12.6",
  "prost-types 0.12.6",
  "regex",
- "syn 2.0.106",
+ "syn 2.0.117",
  "tempfile",
 ]
 
@@ -13392,7 +13445,7 @@ dependencies = [
  "itertools 0.12.1",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -13560,6 +13613,12 @@ dependencies = [
  "bytemuck",
 ]
 
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
 [[package]]
 name = "quick-error"
 version = "2.0.1"
@@ -13785,6 +13844,15 @@ dependencies = [
  "rand_core 0.6.4",
 ]
 
+[[package]]
+name = "rand_xorshift"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
+dependencies = [
+ "rand_core 0.9.3",
+]
+
 [[package]]
 name = "random_choice"
 version = "0.3.2"
@@ -13859,7 +13927,7 @@ dependencies = [
  "avif-serialize",
  "imgref",
  "loop9",
- "quick-error",
+ "quick-error 2.0.1",
  "rav1e",
  "rayon",
  "rgb",
@@ -14059,7 +14127,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -14736,7 +14804,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rust-embed-utils",
- "syn 2.0.106",
+ "syn 2.0.117",
  "walkdir",
 ]
 
@@ -14990,6 +15058,18 @@ version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
 
+[[package]]
+name = "rusty-fork"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
+dependencies = [
+ "fnv",
+ "quick-error 1.2.3",
+ "tempfile",
+ "wait-timeout",
+]
+
 [[package]]
 name = "rustybuzz"
 version = "0.20.1"
@@ -15109,7 +15189,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde_derive_internals",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -15170,7 +15250,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -15199,7 +15279,7 @@ dependencies = [
  "proc-macro-error2",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -15241,7 +15321,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "sea-bae",
- "syn 2.0.106",
+ "syn 2.0.117",
  "unicode-ident",
 ]
 
@@ -15426,7 +15506,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -15437,7 +15517,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -15495,7 +15575,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -15635,7 +15715,7 @@ version = "0.1.0"
 dependencies = [
  "quote",
  "settings",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -16009,7 +16089,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -16187,7 +16267,7 @@ version = "0.1.0"
 dependencies = [
  "sqlez",
  "sqlformat",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -16264,7 +16344,7 @@ dependencies = [
  "quote",
  "sqlx-core",
  "sqlx-macros-core",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -16287,7 +16367,7 @@ dependencies = [
  "sqlx-mysql",
  "sqlx-postgres",
  "sqlx-sqlite",
- "syn 2.0.106",
+ "syn 2.0.117",
  "tokio",
  "url",
 ]
@@ -16446,7 +16526,7 @@ checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69"
 dependencies = [
  "proc-macro-error2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -16589,7 +16669,7 @@ dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -16605,6 +16685,7 @@ dependencies = [
  "arrayvec",
  "ctor",
  "log",
+ "proptest",
  "rand 0.9.2",
  "rayon",
  "tracing",
@@ -16890,9 +16971,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.106"
+version = "2.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -16931,7 +17012,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -17371,6 +17452,7 @@ dependencies = [
  "log",
  "parking_lot",
  "postage",
+ "proptest",
  "rand 0.9.2",
  "regex",
  "rope",
@@ -17481,7 +17563,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -17492,7 +17574,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -17513,7 +17595,7 @@ dependencies = [
  "fax",
  "flate2",
  "half",
- "quick-error",
+ "quick-error 2.0.1",
  "weezl",
  "zune-jpeg",
 ]
@@ -17736,7 +17818,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -18074,7 +18156,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -18169,7 +18251,7 @@ checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -18626,7 +18708,7 @@ version = "0.1.0"
 dependencies = [
  "component",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
  "ui",
 ]
 
@@ -18643,6 +18725,12 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "unarray"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+
 [[package]]
 name = "unicase"
 version = "2.8.1"
@@ -18881,7 +18969,7 @@ version = "0.1.0"
 dependencies = [
  "perf",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -19099,6 +19187,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "waker-fn"
 version = "1.2.0"
@@ -19228,7 +19325,7 @@ dependencies = [
  "bumpalo",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
  "wasm-bindgen-shared",
 ]
 
@@ -19524,7 +19621,7 @@ dependencies = [
  "anyhow",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
  "wasmtime-component-util",
  "wasmtime-wit-bindgen",
  "wit-parser 0.229.0",
@@ -19639,7 +19736,7 @@ checksum = "d0963c1438357a3d8c0efe152b4ef5259846c1cf8b864340270744fe5b3bae5e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -20177,7 +20274,7 @@ dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
  "witx",
 ]
 
@@ -20189,7 +20286,7 @@ checksum = "d873bb5b59ca703b5e41562e96a4796d1af61bf4cf80bf8a7abda755a380ec1c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
  "wiggle-generate",
 ]
 
@@ -20412,7 +20509,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -20423,7 +20520,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -20434,7 +20531,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -20445,7 +20542,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -20456,7 +20553,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.106",
+ "syn 2.0.117",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -650,6 +650,9 @@ postage = { version = "0.5", features = ["futures-traits"] }
 pretty_assertions = { version = "1.3.0", features = ["unstable"] }
 proc-macro2 = "1.0.93"
 profiling = "1"
+# replace this with main when #635 is merged
+proptest = { git = "https://github.com/proptest-rs/proptest", rev = "3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b", features = ["attr-macro"] }
+proptest-derive = "0.8.0"
 prost = "0.9"
 prost-build = "0.9"
 prost-types = "0.9"

crates/editor/Cargo.toml 🔗

@@ -26,6 +26,7 @@ test-support = [
     "tree-sitter-rust",
     "tree-sitter-typescript",
     "tree-sitter-html",
+    "proptest",
     "unindent",
 ]
 
@@ -63,6 +64,8 @@ ordered-float.workspace = true
 parking_lot.workspace = true
 pretty_assertions.workspace = true
 project.workspace = true
+proptest = { workspace = true, optional = true }
+proptest-derive = { workspace = true, optional = true }
 rand.workspace = true
 regex.workspace = true
 rpc.workspace = true
@@ -110,6 +113,8 @@ lsp = { workspace = true, features = ["test-support"] }
 markdown = { workspace = true, features = ["test-support"] }
 multi_buffer = { workspace = true, features = ["test-support"] }
 project = { workspace = true, features = ["test-support"] }
+proptest.workspace = true
+proptest-derive.workspace = true
 release_channel.workspace = true
 rand.workspace = true
 semver.workspace = true

crates/editor/src/editor_tests.rs 🔗

@@ -76,6 +76,9 @@ fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<Di
         .display_ranges(&editor.display_snapshot(cx))
 }
 
+#[cfg(any(test, feature = "test-support"))]
+pub mod property_test;
+
 #[gpui::test]
 fn test_edit_events(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

crates/editor/src/editor_tests/property_test.rs 🔗

@@ -0,0 +1,85 @@
+use proptest::prelude::*;
+
+use super::*;
+
+#[derive(Debug, Clone, proptest_derive::Arbitrary)]
+pub enum Direction {
+    Up,
+    Down,
+    Left,
+    Right,
+}
+
+#[derive(Debug, Clone, proptest_derive::Arbitrary)]
+pub enum TestAction {
+    #[proptest(weight = 4)]
+    Type(String),
+    Backspace {
+        #[proptest(strategy = "1usize..100")]
+        count: usize,
+    },
+    Move {
+        #[proptest(strategy = "1usize..100")]
+        count: usize,
+        direction: Direction,
+    },
+}
+
+impl Editor {
+    pub fn apply_test_action(
+        &mut self,
+        action: &TestAction,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match action {
+            TestAction::Type(text) => self.insert(&text, window, cx),
+            TestAction::Backspace { count } => {
+                for _ in 0..*count {
+                    self.delete(&Default::default(), window, cx);
+                }
+            }
+            TestAction::Move { count, direction } => {
+                for _ in 0..*count {
+                    match direction {
+                        Direction::Up => self.move_up(&Default::default(), window, cx),
+                        Direction::Down => self.move_down(&Default::default(), window, cx),
+                        Direction::Left => self.move_left(&Default::default(), window, cx),
+                        Direction::Right => self.move_right(&Default::default(), window, cx),
+                    }
+                }
+            }
+        }
+    }
+}
+
+fn test_actions() -> impl Strategy<Value = Vec<TestAction>> {
+    proptest::collection::vec(any::<TestAction>(), 1..10)
+}
+
+#[gpui::property_test(config = ProptestConfig {cases: 100, ..Default::default()})]
+fn editor_property_test(
+    cx: &mut TestAppContext,
+    #[strategy = test_actions()] actions: Vec<TestAction>,
+) {
+    init_test(cx, |_| {});
+
+    let group_interval = Duration::from_millis(1);
+
+    let buffer = cx.new(|cx| {
+        let mut buf = language::Buffer::local("123456", cx);
+        buf.set_group_interval(group_interval);
+        buf
+    });
+
+    let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
+
+    editor
+        .update(cx, |editor, window, cx| {
+            for action in actions {
+                editor.apply_test_action(&action, window, cx);
+            }
+        })
+        .unwrap();
+}

crates/gpui/Cargo.toml 🔗

@@ -24,6 +24,7 @@ test-support = [
     "http_client/test-support",
     "wayland",
     "x11",
+    "proptest",
 ]
 inspector = ["gpui_macros/inspector"]
 leak-detection = ["backtrace"]
@@ -64,6 +65,7 @@ num_cpus = "1.13"
 parking = "2.0.0"
 parking_lot.workspace = true
 postage.workspace = true
+proptest = { workspace = true, optional = true }
 chrono.workspace = true
 profiling.workspace = true
 rand.workspace = true

crates/gpui/src/gpui.rs 🔗

@@ -54,6 +54,9 @@ mod util;
 mod view;
 mod window;
 
+#[cfg(any(test, feature = "test-support"))]
+pub use proptest;
+
 #[cfg(doc)]
 pub mod _ownership_and_data_flow;
 
@@ -86,7 +89,9 @@ pub use elements::*;
 pub use executor::*;
 pub use geometry::*;
 pub use global::*;
-pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test};
+pub use gpui_macros::{
+    AppContext, IntoElement, Render, VisualContext, property_test, register_action, test,
+};
 pub use gpui_util::arc_cow::ArcCow;
 pub use http_client;
 pub use input::*;

crates/gpui/src/test.rs 🔗

@@ -27,12 +27,43 @@
 //! ```
 use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
 use futures::StreamExt as _;
+use proptest::prelude::{Just, Strategy, any};
 use std::{
     env,
-    panic::{self, RefUnwindSafe},
+    panic::{self, RefUnwindSafe, UnwindSafe},
     pin::Pin,
 };
 
+/// Strategy injected into `#[gpui::property_test]` tests to control the seed
+/// given to the scheduler. Doesn't shrink, since all scheduler seeds are
+/// equivalent in complexity. If `$SEED` is set, it always uses that value.
+pub fn seed_strategy() -> impl Strategy<Value = u64> {
+    match std::env::var("SEED") {
+        Ok(val) => Just(val.parse().unwrap()).boxed(),
+        Err(_) => any::<u64>().no_shrink().boxed(),
+    }
+}
+
+/// Similar to [`run_test`], but only runs the callback once, allowing
+/// [`FnOnce`] callbacks. This is intended for use with the
+/// `gpui::property_test` macro and generally should not be used directly.
+///
+/// Doesn't support many features of [`run_test`], since these are provided by
+/// proptest.
+pub fn run_test_once(seed: u64, test_fn: Box<dyn UnwindSafe + FnOnce(TestDispatcher)>) {
+    let result = panic::catch_unwind(|| {
+        let dispatcher = TestDispatcher::new(seed);
+        let scheduler = dispatcher.scheduler().clone();
+        test_fn(dispatcher);
+        scheduler.end_test();
+    });
+
+    match result {
+        Ok(()) => {}
+        Err(e) => panic::resume_unwind(e),
+    }
+}
+
 /// Run the given test function with the configured parameters.
 /// This is intended for use with the `gpui::test` macro
 /// and generally should not be used directly.

crates/gpui_macros/Cargo.toml 🔗

@@ -24,4 +24,4 @@ quote.workspace = true
 syn.workspace = true
 
 [dev-dependencies]
-gpui = { workspace = true, features = ["inspector"] }
+gpui = { workspace = true, features = ["inspector"] }

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -3,6 +3,7 @@ mod derive_app_context;
 mod derive_into_element;
 mod derive_render;
 mod derive_visual_context;
+mod property_test;
 mod register_action;
 mod styles;
 mod test;
@@ -188,6 +189,79 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
     test::test(args, function)
 }
 
+/// A variant of `#[gpui::test]` that supports property-based testing.
+///
+/// A property test, much like a standard GPUI randomized test, allows testing
+/// claims of the form "for any possible X, Y should hold". For example:
+/// ```
+/// #[gpui::property_test]
+/// fn test_arithmetic(x: i32, y: i32) {
+///     assert!(x == y || x < y || x > y);
+/// }
+/// ```
+/// Standard GPUI randomized tests provide you with an instance of `StdRng` to
+/// generate random data in a controlled manner. Property-based tests have some
+/// advantages, however:
+/// - Shrinking - the harness also understands a notion of the "complexity" of a
+///   particular value. This allows it to find the "simplest possible value that
+///   causes the test to fail".
+/// - Ergonomics/clarity - the property-testing harness will automatically
+///   generate values, removing the need to fill the test body with generation
+///   logic.
+/// - Failure persistence - if a failing seed is identified, it is stored in a
+///   file, which can be checked in, and future runs will check these cases before
+///   future cases.
+///
+/// Property tests work best when all inputs can be generated up-front and kept
+/// in a simple data structure. Sometimes, this isn't possible - for example, if
+/// a test needs to make a random decision based on the current state of some
+/// structure. In this case, a standard GPUI randomized test may be more
+/// suitable.
+///
+/// ## Customizing random values
+///
+/// This macro is based on the [`#[proptest::property_test]`] macro, but handles
+/// some of the same GPUI-specific arguments as `#[gpui::test]`. Specifically,
+/// `&{mut,} TestAppContext` and `BackgroundExecutor` work as normal. `StdRng`
+/// arguments are **explicitly forbidden**, since they break shrinking, and are
+/// a common footgun.
+///
+/// All other arguments are forwarded to the underlying proptest macro.
+///
+/// Note: much of the following is copied from the proptest docs, specifically the
+/// [`#[proptest::property_test]`] macro docs.
+///
+/// Random values of type `T` are generated by a `Strategy<Value = T>` object.
+/// Some types have a canonical `Strategy` - these types also implement
+/// `Arbitrary`. Parameters to a `#[gpui::property_test]`, by default, use a
+/// type's `Arbitrary` implementation. If you'd like to provide a custom
+/// strategy, you can use `#[strategy = ...]` on the argument:
+/// ```
+/// #[gpui::property_test]
+/// fn int_test(#[strategy = 1..10] x: i32, #[strategy = "[a-zA-Z0-9]{20}"] s: String) {
+///   assert!(s.len() > (x as usize));
+/// }
+/// ```
+///
+/// For more information on writing custom `Strategy` and `Arbitrary`
+/// implementations, see [the proptest book][book], and the [`Strategy`] trait.
+///
+/// ## Scheduler
+///
+/// Similar to `#[gpui::test]`, this macro will choose random seeds for the test
+/// scheduler. It uses `.no_shrink()` to tell proptest that all seeds are
+/// roughly equivalent in terms of "complexity". If `$SEED` is set, it will
+/// affect **ONLY** the seed passed to the scheduler. To control other values,
+/// use custom `Strategy`s.
+///
+/// [`#[proptest::property_test]`]: https://docs.rs/proptest/latest/proptest/attr.property_test.html
+/// [book]: https://proptest-rs.github.io/proptest/intro.html
+/// [`Strategy`]: https://docs.rs/proptest/latest/proptest/strategy/trait.Strategy.html
+#[proc_macro_attribute]
+pub fn property_test(args: TokenStream, function: TokenStream) -> TokenStream {
+    property_test::test(args.into(), function.into()).into()
+}
+
 /// When added to a trait, `#[derive_inspector_reflection]` generates a module which provides
 /// enumeration and lookup by name of all methods that have the shape `fn method(self) -> Self`.
 /// This is used by the inspector so that it can use the builder methods in `Styled` and

crates/gpui_macros/src/property_test.rs 🔗

@@ -0,0 +1,199 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote, quote_spanned};
+use syn::{
+    FnArg, Ident, ItemFn, Type, parse2, punctuated::Punctuated, spanned::Spanned, token::Comma,
+};
+
+pub fn test(args: TokenStream, item: TokenStream) -> TokenStream {
+    let item_span = item.span();
+    let Ok(func) = parse2::<ItemFn>(item) else {
+        return quote_spanned! { item_span =>
+            compile_error!("#[gpui::property_test] must be placed on a function");
+        };
+    };
+
+    let test_name = func.sig.ident.clone();
+    let inner_fn_name = format_ident!("__{test_name}");
+
+    let parsed_args = parse_args(func.sig.inputs, &test_name);
+
+    let inner_body = func.block;
+    let inner_arg_decls = parsed_args.inner_fn_decl_args;
+    let asyncness = func.sig.asyncness;
+
+    let inner_fn = quote! {
+        let #inner_fn_name = #asyncness move |#inner_arg_decls| #inner_body;
+    };
+
+    let arg_errors = parsed_args.errors;
+    let proptest_args = parsed_args.proptest_args;
+    let inner_args = parsed_args.inner_fn_args;
+    let cx_vars = parsed_args.cx_vars;
+    let cx_teardowns = parsed_args.cx_teardowns;
+
+    let proptest_args = quote! {
+        #[strategy = ::gpui::seed_strategy()] __seed: u64,
+        #proptest_args
+    };
+
+    let run_test_body = match &asyncness {
+        None => quote! {
+            #cx_vars
+            #inner_fn_name(#inner_args);
+            #cx_teardowns
+        },
+        Some(_) => quote! {
+            let foreground_executor = gpui::ForegroundExecutor::new(std::sync::Arc::new(dispatcher.clone()));
+            #cx_vars
+            foreground_executor.block_test(#inner_fn_name(#inner_args));
+            #cx_teardowns
+        },
+    };
+
+    quote! {
+        #arg_errors
+
+        #[::gpui::proptest::property_test(proptest_path = "::gpui::proptest", #args)]
+        fn #test_name(#proptest_args) {
+            #inner_fn
+
+            ::gpui::run_test_once(
+                __seed,
+                Box::new(move |dispatcher| {
+                    #run_test_body
+                }),
+            )
+        }
+    }
+}
+
+#[derive(Default)]
+struct ParsedArgs {
+    cx_vars: TokenStream,
+    cx_teardowns: TokenStream,
+    proptest_args: TokenStream,
+    errors: TokenStream,
+
+    // exprs passed at the call-site
+    inner_fn_args: TokenStream,
+    // args in the declaration
+    inner_fn_decl_args: TokenStream,
+}
+
+fn parse_args(args: Punctuated<FnArg, Comma>, test_name: &Ident) -> ParsedArgs {
+    let mut parsed = ParsedArgs::default();
+    let mut args = args.into_iter().collect();
+
+    remove_cxs(&mut parsed, &mut args, test_name);
+    remove_std_rng(&mut parsed, &mut args);
+    remove_background_executor(&mut parsed, &mut args);
+
+    // all remaining args forwarded to proptest's macro
+    parsed.proptest_args = quote!( #(#args),* );
+
+    parsed
+}
+
+fn remove_cxs(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>, test_name: &Ident) {
+    let mut ix = 0;
+    args.retain_mut(|arg| {
+        if !is_test_cx(arg) {
+            return true;
+        }
+
+        let cx_varname = format_ident!("cx_{ix}");
+        ix += 1;
+
+        parsed.cx_vars.extend(quote!(
+            let mut #cx_varname = gpui::TestAppContext::build(
+                dispatcher.clone(),
+                Some(stringify!(#test_name)),
+            );
+        ));
+        parsed.cx_teardowns.extend(quote!(
+            dispatcher.run_until_parked();
+            #cx_varname.executor().forbid_parking();
+            #cx_varname.quit();
+            dispatcher.run_until_parked();
+        ));
+
+        parsed.inner_fn_decl_args.extend(quote!(#arg,));
+        parsed.inner_fn_args.extend(quote!(&mut #cx_varname,));
+
+        false
+    });
+}
+
+fn remove_std_rng(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>) {
+    args.retain_mut(|arg| {
+        if !is_std_rng(arg) {
+            return true;
+        }
+
+        parsed.errors.extend(quote_spanned! { arg.span() =>
+            compile_error!("`StdRng` is not allowed in a property test. Consider implementing `Arbitrary`, or implementing a custom `Strategy`. https://altsysrq.github.io/proptest-book/proptest/tutorial/strategy-basics.html");
+        });
+
+        false
+    });
+}
+
+fn remove_background_executor(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>) {
+    args.retain_mut(|arg| {
+        if !is_background_executor(arg) {
+            return true;
+        }
+
+        parsed.inner_fn_decl_args.extend(quote!(#arg,));
+        parsed
+            .inner_fn_args
+            .extend(quote!(gpui::BackgroundExecutor::new(std::sync::Arc::new(
+                dispatcher.clone()
+            )),));
+
+        false
+    });
+}
+
+// Matches `&TestAppContext` or `&foo::bar::baz::TestAppContext`
+fn is_test_cx(arg: &FnArg) -> bool {
+    let FnArg::Typed(arg) = arg else {
+        return false;
+    };
+
+    let Type::Reference(ty) = &*arg.ty else {
+        return false;
+    };
+
+    let Type::Path(ty) = &*ty.elem else {
+        return false;
+    };
+
+    ty.path
+        .segments
+        .last()
+        .is_some_and(|seg| seg.ident == "TestAppContext")
+}
+
+fn is_std_rng(arg: &FnArg) -> bool {
+    is_path_with_last_segment(arg, "StdRng")
+}
+
+fn is_background_executor(arg: &FnArg) -> bool {
+    is_path_with_last_segment(arg, "BackgroundExecutor")
+}
+
+fn is_path_with_last_segment(arg: &FnArg, last_segment: &str) -> bool {
+    let FnArg::Typed(arg) = arg else {
+        return false;
+    };
+
+    let Type::Path(ty) = &*arg.ty else {
+        return false;
+    };
+
+    ty.path
+        .segments
+        .last()
+        .is_some_and(|seg| seg.ident == last_segment)
+}

crates/sum_tree/Cargo.toml 🔗

@@ -19,11 +19,17 @@ rayon.workspace = true
 log.workspace = true
 ztracing.workspace = true
 tracing.workspace = true
+proptest = { workspace = true, optional = true }
 
 [dev-dependencies]
 ctor.workspace = true
 rand.workspace = true
+proptest.workspace = true
 zlog.workspace = true
 
+
 [package.metadata.cargo-machete]
 ignored = ["tracing"]
+
+[features]
+test-support = ["proptest"]

crates/sum_tree/src/property_test.rs 🔗

@@ -0,0 +1,32 @@
+use core::fmt::Debug;
+
+use proptest::{prelude::*, sample::SizeRange};
+
+use crate::{Item, SumTree, Summary};
+
+impl<T> Arbitrary for SumTree<T>
+where
+    T: Debug + Arbitrary + Item + 'static,
+    T::Summary: Debug + Summary<Context<'static> = ()>,
+{
+    type Parameters = ();
+    type Strategy = BoxedStrategy<Self>;
+
+    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
+        any::<Vec<T>>()
+            .prop_map(|vec| SumTree::from_iter(vec, ()))
+            .boxed()
+    }
+}
+
+/// A strategy for producing a [`SumTree`] with a given size.
+///
+/// Equivalent to [`proptest::collection::vec`].
+pub fn sum_tree<S, T>(values: S, size: impl Into<SizeRange>) -> impl Strategy<Value = SumTree<T>>
+where
+    T: Debug + Arbitrary + Item + 'static,
+    T::Summary: Debug + Summary<Context<'static> = ()>,
+    S: Strategy<Value = T>,
+{
+    proptest::collection::vec(values, size).prop_map(|vec| SumTree::from_iter(vec, ()))
+}

crates/text/Cargo.toml 🔗

@@ -37,3 +37,4 @@ rand.workspace = true
 util = { workspace = true, features = ["test-support"] }
 http_client = { workspace = true, features = ["test-support"] }
 zlog.workspace = true
+proptest.workspace = true