proptest-state-machine-0.4.0/.cargo_vcs_info.json0000644000000001640000000000100154360ustar { "git": { "sha1": "c073d523dcecb5fe3e38fa7ec30ed3df06441cc1" }, "path_in_vcs": "proptest-state-machine" }proptest-state-machine-0.4.0/CHANGELOG.md000064400000000000000000000023471046102023000160440ustar 00000000000000## Unreleased ## 0.4.0 ### Other Notes - Set MSRV to 1.82, which is what minimally compiles and completes testing. - Updated `rand` dependency from 0.8 to 0.9. ## 0.3.1 - Fixed checking of pre-conditions with a shrinked or complicated initial state. ([\#482](https://github.com/proptest-rs/proptest/pull/482)) ## 0.3.0 ### New Features - Remove unseen transitions on a first step of shrinking. ([\#388](https://github.com/proptest-rs/proptest/pull/388)) ## 0.2.0 ### Other Notes - `message-io` updated from 0.17 to 0.18 ### Bug Fixes - Removed the limit of number of transitions that can be deleted in shrinking that depended on the number the of transitions given to `prop_state_machine!` or `ReferenceStateMachine::sequential_strategy`. - Fixed state-machine macro's inability to handle missing config - Fixed logging of state machine transitions to be enabled when verbose config is >= 1. The "std" feature is added to proptest-state-machine as a default feature that allows to switch the logging off in non-std env. - Fixed an issue where after simplification of the initial state causes the test to succeed, the initial state would not be re-complicated - causing the test to report a succeeding input as the simplest failing input. proptest-state-machine-0.4.0/Cargo.lock0000644000000533650000000000100134240ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crossbeam-channel" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" dependencies = [ "cfg-if", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "integer-encoding" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "linux-raw-sys" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "message-io" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4e0a0650c289b1c5f75d6085960d4fc45e5ca2bfbb9eaf192958777ed83051f" dependencies = [ "crossbeam-channel", "crossbeam-utils", "integer-encoding", "lazy_static", "libc", "log", "mio", "nix", "serde", "socket2", "strum", "tungstenite", "url", ] [[package]] name = "mio" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] [[package]] name = "nix" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset", ] [[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set", "bit-vec", "bitflags 2.9.1", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "proptest-state-machine" version = "0.4.0" dependencies = [ "message-io", "proptest", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", ] [[package]] name = "rand_xorshift" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ "rand_core", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustversion" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "serde" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", "syn 2.0.101", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "socket2" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", ] [[package]] name = "strum" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", "syn 1.0.109", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn 2.0.101", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tungstenite" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", "http", "httparse", "log", "rand", "sha1", "thiserror", "url", "utf-8", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-bidi" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.1", ] proptest-state-machine-0.4.0/Cargo.toml0000644000000033050000000000100134340ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.82" name = "proptest-state-machine" version = "0.4.0" authors = ["Tomáš Zemanovič"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = """ State machine based testing support for proptest. """ homepage = "https://proptest-rs.github.io/proptest/proptest/state-machine.html" documentation = "https://docs.rs/proptest-state-machine/latest/proptest-state-machine/" readme = "README.md" keywords = [ "property", "testing", "quickcheck", "fuzz", "state-machine", ] categories = ["development-tools::testing"] license = "MIT OR Apache-2.0" repository = "https://github.com/proptest-rs/proptest" [features] default = ["std"] std = ["proptest/std"] [lib] name = "proptest_state_machine" path = "src/lib.rs" [[example]] name = "state_machine_echo_server" path = "examples/state_machine_echo_server.rs" [[example]] name = "state_machine_heap" path = "examples/state_machine_heap.rs" [dependencies.proptest] version = "1.7.0" features = [ "fork", "timeout", "bit-set", ] default-features = true [dev-dependencies.message-io] version = "0.19.0" features = [ "tcp", "udp", "websocket", ] default-features = false proptest-state-machine-0.4.0/Cargo.toml.orig000064400000000000000000000015761046102023000171250ustar 00000000000000[package] name = "proptest-state-machine" version = "0.4.0" authors = ["Tomáš Zemanovič"] license = "MIT OR Apache-2.0" edition = "2021" rust-version = "1.82" repository = "https://github.com/proptest-rs/proptest" homepage = "https://proptest-rs.github.io/proptest/proptest/state-machine.html" documentation = "https://docs.rs/proptest-state-machine/latest/proptest-state-machine/" keywords = ["property", "testing", "quickcheck", "fuzz", "state-machine"] categories = ["development-tools::testing"] description = """ State machine based testing support for proptest. """ [features] default = ["std"] # Enables the use of standard-library dependent features std = ["proptest/std"] [dependencies] proptest = { version = "1.7.0", path = "../proptest", default-features = true, features = [ "fork", "timeout", "bit-set", ] } [dev-dependencies] message-io = { workspace = true } proptest-state-machine-0.4.0/README.md000064400000000000000000000004321046102023000155030ustar 00000000000000# proptest-state-machine The state machine testing support provides a strategy and convenience runner macro for a sequential state machine. To learn more, please consult [state machine page in the Proptest book](https://proptest-rs.github.io/proptest/proptest/state-machine.html). proptest-state-machine-0.4.0/examples/state_machine_echo_server.rs000064400000000000000000000413261046102023000236070ustar 00000000000000//- // Copyright 2023 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! In this example, we're using the state machine testing to test interactions //! of arbitrary client with an echo server, implemented using `message-io` //! crate in the `system_under_test` module. #[macro_use] extern crate proptest_state_machine; use std::collections::{HashMap, HashSet}; use std::thread; use proptest::prelude::*; use proptest::test_runner::Config; use proptest_state_machine::{ReferenceStateMachine, StateMachineTest}; use system_under_test::{ init_client, init_server, run_client, run_server, ClientDialer, Msg, ServerDialer, Transport, }; // Setup the state machine test using the `prop_state_machine!` macro prop_state_machine! { #![proptest_config(Config { // Turn failure persistence off for demonstration. This means that no // regression file will be captured. failure_persistence: None, // Enable verbose mode to make the state machine test print the // transitions for each case. verbose: 1, // Only run 10 cases by default to avoid running out of system resources // and taking too long to finish. cases: 10, .. Config::default() })] // NOTE: The `#[test]` attribute is commented out in here so we can run it // as an example from the `fn main`. // #[test] fn run_echo_server_test( // This is a macro's keyword - only `sequential` is currently supported. sequential // The number of transitions to be generated for each case. This can // be a single numerical value or a range as in here. 1..20 // Macro's boilerplate to separate the following identifier. => // The name of the type that implements `StateMachineTest`. EchoServerTest ); } fn main() { run_echo_server_test(); } /// The reference state of the server and clients. #[derive(Clone, Debug)] struct RefState { /// The server status. is_server_up: bool, /// Set of client IDs that are connected. clients: HashSet, /// We randomly select which transport to use for the test case. transport: Transport, } /// The possible transitions of the state machine. #[derive(Clone, Debug)] enum Transition { StartServer, StopServer, StartClient(ClientId), StopClient(ClientId), ClientMsg(ClientId, Msg), } /// The state of the concrete server and clients under test. #[derive(Default)] struct EchoServerTest { server: Option, clients: HashMap, } struct TestServer { /// A server dialer can be used to send message to clients and to shut-down /// the server. dialer: ServerDialer, /// The a handle of a thread that runs the server listener. listener_handle: thread::JoinHandle<()>, } struct TestClient { /// A client dialer can send messages to the server. dialer: ClientDialer, /// A handle of a thread that runs the client listener. listener_handle: std::thread::JoinHandle<()>, /// Messages received by the listener of the server are forwarded to this /// receiver, to be checked by the test. msgs_recv: std::sync::mpsc::Receiver, } type ClientId = usize; impl ReferenceStateMachine for RefState { type State = RefState; type Transition = Transition; fn init_state() -> BoxedStrategy { prop_oneof![ Just(Transport::Tcp), Just(Transport::FramedTcp), Just(Transport::Udp), Just(Transport::Ws), ] .prop_map(|transport| Self { is_server_up: false, clients: HashSet::default(), transport, }) .boxed() } fn transitions(state: &Self::State) -> BoxedStrategy { use Transition::*; if state.clients.is_empty() { prop_oneof![ Just(StartServer), Just(StopServer), (0..32_usize).prop_map(StartClient), ] .boxed() } else { let ids: Vec<_> = state.clients.iter().cloned().collect(); let arb_id = proptest::sample::select(ids); prop_oneof![ Just(StartServer), Just(StopServer), (0..32_usize).prop_map(StartClient), arb_id.clone().prop_map(StopClient), arb_id.prop_flat_map(|id| arb_msg_from_client() .prop_map(move |msg| { ClientMsg(id, msg) })), ] .boxed() } } fn apply( mut state: Self::State, transition: &Self::Transition, ) -> Self::State { match transition { Transition::StartServer => { state.is_server_up = true; } Transition::StopServer => { state.is_server_up = false; // Any existing clients will be disconnected. state.clients = Default::default(); } Transition::StartClient(id) => { state.clients.insert(*id); } Transition::StopClient(id) => { state.clients.remove(id); } Transition::ClientMsg(_id, _msg) => { // Nothing to do in reference state. } } state } fn preconditions( state: &Self::State, transition: &Self::Transition, ) -> bool { match transition { Transition::StartServer => !state.is_server_up, Transition::StopServer => state.is_server_up, Transition::StartClient(id) => { // Only start clients if the server is running and this // client ID is not running already. state.is_server_up && !state.clients.contains(id) } Transition::StopClient(id) => { // Stop only if this client is actually running. state.clients.contains(id) } Transition::ClientMsg(id, _) => { // Can send only if both the server and this client are running. state.is_server_up && state.clients.contains(id) } } } } /// Generate an arbitrary MsgFromClient fn arb_msg_from_client() -> impl Strategy { "[a-z0-9]{1,8}" } impl StateMachineTest for EchoServerTest { type SystemUnderTest = Self; type Reference = RefState; fn init_test( _ref_state: &::State, ) -> Self::SystemUnderTest { Self::default() } fn apply( mut state: Self::SystemUnderTest, ref_state: &::State, transition: ::Transition, ) -> Self::SystemUnderTest { match transition { Transition::StartServer => { // Assign port dynamically let (dialer, listener) = init_server(ref_state.transport, "127.0.0.1:0"); // Run the listener in a new thread let listener_handle = thread::spawn(move || run_server(listener)); state.server = Some(TestServer { dialer, listener_handle, }) } Transition::StopServer => { let server = state.server.take().unwrap(); server.dialer.handler.stop(); // Wait for the server listener to stop server.listener_handle.join().unwrap(); if !state.clients.is_empty() { println!( "The server is waiting for all the clients to \ stop..." ); for (id, client) in std::mem::take(&mut state.clients).into_iter() { // Ask the client to stop client.dialer.handler.stop(); println!("Asking client {} listener to stop.", id); // Wait for it to actually stop client.listener_handle.join().unwrap(); println!("Client {} listener stopped.", id); } println!("All clients have stopped."); } } Transition::StartClient(id) => { // Get the address of the server. let server_addr = state.server.as_ref().unwrap().dialer.address; let (listener, dialer) = init_client(ref_state.transport, server_addr); // Open a channel for receiving message from the listener, so // that we can check the response the server. let (msgs_send, msgs_recv) = std::sync::mpsc::channel(); let listener_handle = std::thread::spawn(move || { run_client(listener, |msg| { msgs_send.send(msg).unwrap(); }) }); state.clients.insert( id, TestClient { dialer, listener_handle, msgs_recv, }, ); } Transition::StopClient(id) => { // Remove the client let client = state.clients.remove(&id).unwrap(); // Ask the client to stop client.dialer.handler.stop(); // Wait for it to actually stop client.listener_handle.join().unwrap(); } Transition::ClientMsg(id, msg) => { let client = state.clients.get_mut(&id).unwrap(); // We use the broken implementation of msg_server, which should // be discovered by the test. system_under_test::msg_server_wrong(&mut client.dialer, &msg); // NOTE: To fix the issue that gets found by the state machine, // you can comment out the last statement with `pop_wrong` and // uncomment this one to see the test pass: // system_under_test::msg_server(&mut client.dialer, &msg); // Post-condition: The server must send a response back to the // client println!("Waiting for server response."); println!( "WARN: Because we're using a blocking call here, this will \ halt when the message gets lost when `msg_server_wrong` is used." ); let recv_msg = client.msgs_recv.recv().unwrap(); assert_eq!(recv_msg, msg) } } state } } mod system_under_test { pub use message_io::network::Transport; use message_io::network::{Endpoint, NetEvent, ResourceId, ToRemoteAddr}; use message_io::node::{self, NodeEvent, NodeHandler, NodeListener}; use std::net::{SocketAddr, ToSocketAddrs}; use std::sync::atomic::{self, AtomicBool}; use std::sync::Arc; const ATOMIC_ORDER: atomic::Ordering = atomic::Ordering::SeqCst; /// We're only using valid UTF-8 strings here for messages to avoid having /// to pull another dev-dependency for serialization. pub type Msg = String; pub struct ServerListener { pub listener: NodeListener<()>, pub handler: NodeHandler<()>, } pub struct ServerDialer { pub address: SocketAddr, pub resource_id: ResourceId, pub handler: NodeHandler<()>, } pub struct ClientListener { pub address: SocketAddr, pub listener: NodeListener<()>, pub server: Endpoint, pub handler: NodeHandler<()>, /// Server connection status, shared with the [`ClientDialer`]. pub is_connected: Arc, } pub struct ClientDialer { pub address: SocketAddr, pub server: Endpoint, pub handler: NodeHandler<()>, /// Server connection status, shared with the [`ClientListener`]. pub is_connected: Arc, } pub fn init_server( transport: Transport, addr: impl ToSocketAddrs, ) -> (ServerDialer, ServerListener) { let (handler, listener) = node::split::<()>(); let (resource_id, address) = handler.network().listen(transport, addr).unwrap(); println!("Server is running at {address} with {transport}."); ( ServerDialer { address, resource_id, handler: handler.clone(), }, ServerListener { listener, handler }, ) } pub fn run_server(listener: ServerListener) { let ServerListener { listener, handler } = listener; listener.for_each(move |event| match event.network() { NetEvent::Connected(_, _) => (), // Only generated at connect() calls. NetEvent::Accepted(endpoint, _resource_id) => { // Only connection oriented protocols will generate this event println!("Client ({}) connected.", endpoint.addr(),); } NetEvent::Message(endpoint, msg_bytes) => { let message: Msg = String::from_utf8(msg_bytes.to_vec()).unwrap(); println!("Server received a message \"{message}\"."); handler.network().send(endpoint, msg_bytes); } NetEvent::Disconnected(endpoint) => { // Only connection oriented protocols will generate this event println!("Client ({}) disconnected.", endpoint.addr()); } }); } pub fn init_client( transport: Transport, remote_addr: impl ToRemoteAddr, ) -> (ClientListener, ClientDialer) { let (handler, listener) = node::split(); let (server, address) = handler.network().connect(transport, remote_addr).unwrap(); let is_connected = Arc::new(AtomicBool::new(false)); ( ClientListener { address, server, handler: handler.clone(), listener, is_connected: is_connected.clone(), }, ClientDialer { address, server, handler, is_connected, }, ) } pub fn run_client(listener: ClientListener, mut on_msg: impl FnMut(Msg)) { let ClientListener { address, server, handler, listener, is_connected, } = listener; listener.for_each(move |event| match event { NodeEvent::Network(net_event) => match net_event { NetEvent::Connected(_, established) => { if established { println!( "Client identified by local port: {}.", address.port() ); } else { println!("Cannot connect to server at {server}.") } is_connected.store(established, ATOMIC_ORDER); } NetEvent::Accepted(_, _) => unreachable!(), // Only generated when a listener accepts NetEvent::Message(_, msg_bytes) => { let message: Msg = String::from_utf8(msg_bytes.to_vec()).unwrap(); on_msg(message); } NetEvent::Disconnected(_) => { println!("Server is disconnected."); is_connected.store(false, ATOMIC_ORDER); handler.stop(); } }, NodeEvent::Signal(()) => { // unused } }); } /// This function will lose messages when they are sent before the client /// connection is established. pub fn msg_server_wrong(dialer: &mut ClientDialer, msg: &Msg) { let output_data = msg.as_bytes(); dialer.handler.network().send(dialer.server, output_data); } #[allow(dead_code)] pub fn msg_server(dialer: &mut ClientDialer, msg: &Msg) { let output_data = msg.as_bytes(); while !dialer.is_connected.load(ATOMIC_ORDER) { println!("Waiting for the server to be ready."); } dialer.handler.network().send(dialer.server, &output_data); } } proptest-state-machine-0.4.0/examples/state_machine_heap.rs000064400000000000000000000172531046102023000222220ustar 00000000000000//- // Copyright 2023 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! In this example, we demonstrate using the state machine testing approach //! for a heap implementation that has a bug in it. The heap `MyHeap` is in the //! `system_under_test` module inlined at the bottom of this file. #[macro_use] extern crate proptest_state_machine; use proptest::prelude::*; use proptest::test_runner::Config; use proptest_state_machine::{ReferenceStateMachine, StateMachineTest}; use system_under_test::MyHeap; // Setup the state machine test using the `prop_state_machine!` macro prop_state_machine! { #![proptest_config(Config { // Turn failure persistence off for demonstration. This means that no // regression file will be captured. failure_persistence: None, // Enable verbose mode to make the state machine test print the // transitions for each case. verbose: 1, .. Config::default() })] // NOTE: The `#[test]` attribute is commented out in here so we can run it // as an example from the `fn main`. // #[test] fn run_my_heap_test( // This is a macro's keyword - only `sequential` is currently supported. sequential // The number of transitions to be generated for each case. This can // be a single numerical value or a range as in here. 1..20 // Macro's boilerplate to separate the following identifier. => // The name of the type that implements `StateMachineTest`. MyHeap ); } fn main() { run_my_heap_test(); } /// An empty type used for the `ReferenceStateMachine` implementation. The /// actual state of it represented by `Vec`, but it doesn't have to /// contained inside this type. pub struct HeapStateMachine; /// The possible transitions of the state machine. #[derive(Clone, Debug)] pub enum Transition { Pop, Push(i32), } // Implementation of the reference state machine that drives the test. That is, // it's used to generate a sequence of transitions the `StateMachineTest`. impl ReferenceStateMachine for HeapStateMachine { type State = Vec; type Transition = Transition; fn init_state() -> BoxedStrategy { Just(vec![]).boxed() } fn transitions(_state: &Self::State) -> BoxedStrategy { // Using the regular proptest constructs here, the transitions can be // given different weights. prop_oneof![ 1 => Just(Transition::Pop), 2 => (any::()).prop_map(Transition::Push), ] .boxed() } fn apply( mut state: Self::State, transition: &Self::Transition, ) -> Self::State { match transition { Transition::Pop => { state.pop(); } Transition::Push(value) => state.push(*value), } state } } impl StateMachineTest for MyHeap { type SystemUnderTest = Self; type Reference = HeapStateMachine; fn init_test( _ref_state: &::State, ) -> Self::SystemUnderTest { MyHeap::new() } fn apply( mut state: Self::SystemUnderTest, _ref_state: &::State, transition: Transition, ) -> Self::SystemUnderTest { match transition { Transition::Pop => { // We read the state before applying the transition. let was_empty = state.is_empty(); // We use the broken implementation of pop, which should be // discovered by the test. let result = state.pop_wrong(); // NOTE: To fix the issue that gets found by the state machine, // you can comment out the last statement with `pop_wrong` and // uncomment this one to see the test pass: // let result = state.pop(); // Check a post-condition. match result { Some(value) => { assert!(!was_empty); // The heap must not contain any value which was // greater than the "maximum" we were just given. for in_heap in state.iter() { assert!( value >= *in_heap, "Popped value {:?}, which was less \ than {:?} still in the heap", value, in_heap ); } } None => assert!(was_empty), } } Transition::Push(value) => state.push(value), } state } fn check_invariants( state: &Self::SystemUnderTest, _ref_state: &::State, ) { // Check that the heap's API gives consistent results assert_eq!(0 == state.len(), state.is_empty()); } } /// A hand-rolled implementation of a binary heap, like /// , /// except slow and buggy. mod system_under_test { use std::cmp; #[derive(Clone, Debug)] pub struct MyHeap { data: Vec, } impl MyHeap { pub fn new() -> Self { MyHeap { data: vec![] } } pub fn is_empty(&self) -> bool { self.data.is_empty() } pub fn len(&self) -> usize { self.data.len() } pub fn iter(&self) -> impl Iterator { self.data.iter() } pub fn push(&mut self, value: T) { self.data.push(value); let mut index = self.data.len() - 1; while index > 0 { let parent = (index - 1) / 2; if self.data[parent] < self.data[index] { self.data.swap(index, parent); index = parent; } else { break; } } } // This implementation is wrong, because it doesn't preserve ordering pub fn pop_wrong(&mut self) -> Option { if self.is_empty() { None } else { Some(self.data.swap_remove(0)) } } // Fixed implementation of pop() #[allow(dead_code)] pub fn pop(&mut self) -> Option { if self.is_empty() { return None; } let ret = self.data.swap_remove(0); // Restore the heap property let mut index = 0; loop { let child1 = index * 2 + 1; let child2 = index * 2 + 2; if child1 >= self.data.len() { break; } let child = if child2 == self.data.len() || self.data[child1] > self.data[child2] { child1 } else { child2 }; if self.data[index] < self.data[child] { self.data.swap(child, index); index = child; } else { break; } } Some(ret) } } } proptest-state-machine-0.4.0/proptest-regressions/strategy.txt000064400000000000000000000013761046102023000230600ustar 00000000000000# Seeds for failure cases proptest has generated in the past. It is # automatically read and these particular cases re-run before any # novel cases are generated. # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc 2e5fb0b2e70b08bd8a567bf4d9ae66ee2478d832d6bdfb9670f5b98203e78178 # shrinks to seed = [120, 233, 214, 148, 193, 171, 178, 64, 157, 62, 78, 165, 215, 79, 177, 175, 171, 202, 51, 93, 79, 238, 39, 104, 174, 79, 152, 255, 45, 174, 27, 168] cc 39927f23e5f67ac32c5219c226c49d87e3e3c995a73cd969d72fbcdf52ac895b # shrinks to seed = [0, 10, 244, 19, 249, 125, 161, 150, 61, 56, 77, 245, 12, 228, 187, 180, 148, 61, 17, 32, 189, 118, 70, 47, 147, 210, 94, 127, 210, 23, 128, 75] proptest-state-machine-0.4.0/src/lib.rs000064400000000000000000000011711046102023000161300ustar 00000000000000//- // Copyright 2023 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Strategies and test runners for Proptest State Machine tests. //! //! Please refer to the Proptest Book chapter "State Machine testing" to learn //! when and how to use this and how it's made. pub mod strategy; pub mod test_runner; pub use strategy::*; pub use test_runner::*; proptest-state-machine-0.4.0/src/strategy.rs000064400000000000000000001230501046102023000172250ustar 00000000000000//- // Copyright 2023 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Strategies used for abstract state machine testing. use std::sync::atomic::{self, AtomicUsize}; use std::sync::Arc; use proptest::bits::{BitSetLike, VarBitSet}; use proptest::collection::SizeRange; use proptest::num::sample_uniform_incl; use proptest::std_facade::fmt::{Debug, Formatter, Result}; use proptest::std_facade::Vec; use proptest::strategy::BoxedStrategy; use proptest::strategy::{NewTree, Strategy, ValueTree}; use proptest::test_runner::TestRunner; /// This trait is used to model system under test as an abstract state machine. /// /// The key to how this works is that the set of next valid transitions depends /// on its current state (it's not the same as generating a random sequence of /// transitions) and just like other prop strategies, the state machine strategy /// attempts to shrink the transitions to find the minimal reproducible example /// when it encounters a case that breaks any of the defined properties. /// /// This is achieved with the [`ReferenceStateMachine::transitions`] that takes /// the current state as an argument and can be used to decide which transitions /// are valid from this state, together with the /// [`ReferenceStateMachine::preconditions`], which are checked during generation /// of transitions and during shrinking. /// /// Hence, the `preconditions` only needs to contain checks for invariants that /// depend on the current state and may be broken by shrinking and it doesn't /// need to cover invariants that do not depend on the current state. /// /// The reference state machine generation runs before the generated transitions /// are attempted to be executed against the SUT (the concrete state machine) /// as defined by [`crate::StateMachineTest`]. pub trait ReferenceStateMachine { /// The reference state machine's state type. This should contain the minimum /// required information needed to implement the state machine. It is used /// to drive the generations of transitions to decide which transitions are /// valid for the current state. type State: Clone + Debug; /// The reference state machine's transition type. This is typically an enum /// with its variants containing the parameters required to apply the /// transition, if any. type Transition: Clone + Debug; // TODO Instead of the boxed strategies, this could use // once stabilized: // type StateStrategy = impl Strategy; // type TransitionStrategy = impl Strategy; /// The initial state may be generated by any strategy. For a constant /// initial state, use [`proptest::strategy::Just`]. fn init_state() -> BoxedStrategy; /// Generate the initial transitions. fn transitions(state: &Self::State) -> BoxedStrategy; /// Apply a transition in the reference state. fn apply(state: Self::State, transition: &Self::Transition) -> Self::State; /// Pre-conditions may be specified to control which transitions are valid /// from the current state. If not overridden, this allows any transition. /// The pre-conditions are checked in the generated transitions and during /// shrinking. /// /// The pre-conditions checking relies on proptest global rejection /// filtering, which comes with some [disadvantages](https://altsysrq.github.io/proptest-book/proptest/tutorial/filtering.html). /// This means that pre-conditions that are hard to satisfy might slow down /// the test or even fail by exceeding the maximum rejection count. fn preconditions( state: &Self::State, transition: &Self::Transition, ) -> bool { // This is to avoid `unused_variables` warning let _ = (state, transition); true } /// A sequential strategy runs the state machine transitions generated from /// the reference model sequentially in a test over a concrete state, which /// can be implemented with the help of /// [`crate::StateMachineTest`] trait. /// /// You typically never need to override this method. fn sequential_strategy( size: impl Into, ) -> Sequential< Self::State, Self::Transition, BoxedStrategy, BoxedStrategy, > { Sequential::new( size.into(), Self::init_state, Self::preconditions, Self::transitions, Self::apply, ) } } /// In a sequential state machine strategy, we first generate an acceptable /// sequence of transitions. That is a sequence that satisfies the given /// pre-conditions. The acceptability of each transition in the sequence depends /// on the current state of the state machine, which is updated by the /// transitions with the `next` function. /// /// The shrinking strategy is to iteratively apply `Shrink::InitialState`, /// `Shrink::DeleteTransition` and `Shrink::Transition`. /// /// 1. We start by trying to delete transitions from the back of the list that /// were never seen by the test, if any. Note that because proptest expects /// deterministic results in for reproducible issues, unlike the following /// steps this step will not be undone on `complicate`. If there were any /// unseen transitions, then the next step will start at trying to delete /// the transition before the last one seen as we know that the last /// transition cannot be deleted as it's the one that has failed. /// 2. Then, we keep trying to delete transitions from the back of the list, until /// we can do so no further (reached the beginning of the list).. /// 3. Then, we again iteratively attempt to shrink the individual transitions, /// but this time starting from the front of the list - i.e. from the first /// transition to be applied. /// 4. Finally, we try to shrink the initial state until it's not possible to /// shrink it any further. /// /// For `complicate`, we attempt to undo the last shrink operation, if there was /// any. pub struct Sequential { size: SizeRange, init_state: fn() -> StateStrategy, preconditions: fn(state: &State, transition: &Transition) -> bool, transitions: fn(state: &State) -> TransitionStrategy, next: fn(state: State, transition: &Transition) -> State, } impl Sequential { pub fn new( size: SizeRange, init_state: fn() -> StateStrategy, preconditions: fn(state: &State, transition: &Transition) -> bool, transitions: fn(state: &State) -> TransitionStrategy, next: fn(state: State, transition: &Transition) -> State, ) -> Self { Self { size, init_state, preconditions, transitions, next, } } } impl Debug for Sequential { fn fmt(&self, f: &mut Formatter) -> Result { f.debug_struct("Sequential") .field("size", &self.size) .finish() } } impl< State: Clone + Debug, Transition: Clone + Debug, StateStrategy: Strategy, TransitionStrategy: Strategy, > Strategy for Sequential { type Tree = SequentialValueTree< State, Transition, StateStrategy::Tree, TransitionStrategy::Tree, >; type Value = (State, Vec, Option>); fn new_tree(&self, runner: &mut TestRunner) -> NewTree { // Generate the initial state value tree let initial_state = (self.init_state)().new_tree(runner)?; let last_valid_initial_state = initial_state.current(); let (min_size, end) = self.size.start_end_incl(); // Sample the maximum number of the transitions from the size range let max_size = sample_uniform_incl(runner, min_size, end); let mut transitions = Vec::with_capacity(max_size); let mut acceptable_transitions = Vec::with_capacity(max_size); let included_transitions = VarBitSet::saturated(max_size); let shrinkable_transitions = VarBitSet::saturated(max_size); // Sample the transitions until we reach the `max_size` let mut state = initial_state.current(); while transitions.len() < max_size { // Apply the current state to find the current transition let transition_tree = (self.transitions)(&state).new_tree(runner)?; let transition = transition_tree.current(); // If the pre-conditions are satisfied, use the transition if (self.preconditions)(&state, &transition) { transitions.push(transition_tree); state = (self.next)(state, &transition); acceptable_transitions .push((TransitionState::Accepted, transition)); } else { runner.reject_local("Pre-conditions were not satisfied")?; } } // The maximum index into the vectors and bit sets let max_ix = max_size - 1; Ok(SequentialValueTree { initial_state, is_initial_state_shrinkable: true, last_valid_initial_state, preconditions: self.preconditions, next: self.next, transitions, acceptable_transitions, included_transitions, shrinkable_transitions, max_ix, // On a failure, we start by shrinking transitions from the back // which is less likely to invalidate pre-conditions shrink: Shrink::DeleteTransition(max_ix), last_shrink: None, seen_transitions_counter: Some(Default::default()), }) } } /// A shrinking operation #[derive(Clone, Copy, Debug)] enum Shrink { /// Shrink the initial state InitialState, /// Delete a transition at given index DeleteTransition(usize), /// Shrink a transition at given index Transition(usize), } use Shrink::*; /// The state of a transition in the model #[derive(Clone, Copy, Debug)] enum TransitionState { /// The transition that is equal to the result of `ValueTree::current()` /// and satisfies the pre-conditions Accepted, /// The transition has been simplified, but rejected by pre-conditions SimplifyRejected, /// The transition has been complicated, but rejected by pre-conditions ComplicateRejected, } use TransitionState::*; /// The generated value tree for a sequential state machine. pub struct SequentialValueTree< State, Transition, StateValueTree, TransitionValueTree, > { /// The initial state value tree initial_state: StateValueTree, /// Can the `initial_state` be shrunk any further? is_initial_state_shrinkable: bool, /// The last initial state that has been accepted by the pre-conditions. /// We have to store this every time before attempt to shrink to be able /// to back to it in case the shrinking is rejected. last_valid_initial_state: State, /// The pre-conditions predicate preconditions: fn(&State, &Transition) -> bool, /// The function from current state and a transition to an updated state next: fn(State, &Transition) -> State, /// The list of transitions' value trees transitions: Vec, /// The sequence of included transitions with their shrinking state acceptable_transitions: Vec<(TransitionState, Transition)>, /// The bit-set of transitions that have not been deleted by shrinking included_transitions: VarBitSet, /// The bit-set of transitions that can be shrunk further shrinkable_transitions: VarBitSet, /// The maximum index in the `transitions` vector (its size - 1) max_ix: usize, /// The next shrink operation to apply shrink: Shrink, /// The last applied shrink operation, if any last_shrink: Option, /// The number of transitions that were seen by the test runner. /// On a test run this is shared with `StateMachineTest::test_sequential` /// which increments the inner counter value on every transition. If the /// test fails, the counter is used to remove any unseen transitions before /// shrinking and this field is set to `None` as it's no longer needed for /// shrinking. seen_transitions_counter: Option>, } impl< State: Clone + Debug, Transition: Clone + Debug, StateValueTree: ValueTree, TransitionValueTree: ValueTree, > SequentialValueTree { /// Try to apply the next `self.shrink`. Returns `true` if a shrink has been /// applied. fn try_simplify(&mut self) -> bool { if let Some(seen_transitions_counter) = self.seen_transitions_counter.as_ref() { let seen_count = seen_transitions_counter.load(atomic::Ordering::SeqCst); let included_count = self.included_transitions.count(); if seen_count < included_count { // the test runner did not see all the transitions so we can // delete the transitions that were not seen because they were // not executed let mut kept_count = 0; for ix in 0..self.transitions.len() { if self.included_transitions.test(ix) { // transition at ix was part of test if kept_count < seen_count { // transition at xi was seen by the test or we are // still below minimum size for the test kept_count += 1; } else { // transition at ix was never seen self.included_transitions.clear(ix); self.shrinkable_transitions.clear(ix); } } } // Set the next shrink based on how many transitions were seen: // - If 0 seen: go directly to shrinking the initial state. // - If 1 seen: can't delete any more, so shrink individual transitions. // - If >1 seen: delete the transition before the last seen transition. // (subtract 2 from `kept_count` because the last seen transition // caused the failure). if kept_count == 0 { self.shrink = InitialState; } else if kept_count == 1 { self.shrink = Transition(0); } else { self.shrink = DeleteTransition( kept_count.checked_sub(2).unwrap_or_default(), ); } } // Remove the seen transitions counter for shrinking runs self.seen_transitions_counter = None; } if let DeleteTransition(ix) = self.shrink { // Delete the index from the included transitions self.included_transitions.clear(ix); self.last_shrink = Some(self.shrink); self.shrink = if ix == 0 { // Reached the beginning of the list, move on to shrinking Transition(0) } else { // Try to delete the previous transition next DeleteTransition(ix - 1) }; // If this delete is not acceptable, undo it and try again if !self .check_acceptable(None, self.last_valid_initial_state.clone()) { self.included_transitions.set(ix); self.last_shrink = None; return self.try_simplify(); } // If the delete was accepted, remove this index from shrinkable // transitions self.shrinkable_transitions.clear(ix); return true; } while let Transition(ix) = self.shrink { if self.shrinkable_transitions.count() == 0 { // Move on to shrinking the initial state self.shrink = Shrink::InitialState; break; } if !self.included_transitions.test(ix) { // No use shrinking something we're not including self.shrink = self.next_shrink_transition(ix); continue; } if let Some((SimplifyRejected, _trans)) = self.acceptable_transitions.get(ix) { // This transition is already simplified and rejected self.shrink = self.next_shrink_transition(ix); } else if self.transitions[ix].simplify() { self.last_shrink = Some(self.shrink); if self.check_acceptable( Some(ix), self.last_valid_initial_state.clone(), ) { self.acceptable_transitions[ix] = (Accepted, self.transitions[ix].current()); return true; } else { let (state, _trans) = self.acceptable_transitions.get_mut(ix).unwrap(); *state = SimplifyRejected; self.shrinkable_transitions.clear(ix); self.shrink = self.next_shrink_transition(ix); return self.simplify(); } } else { self.shrinkable_transitions.clear(ix); self.shrink = self.next_shrink_transition(ix); } } if let InitialState = self.shrink { if self.initial_state.simplify() { if self.check_acceptable(None, self.initial_state.current()) { self.last_valid_initial_state = self.initial_state.current(); self.last_shrink = Some(self.shrink); return true; } else { // If the shrink is not acceptable, clear it out self.last_shrink = None; // `initial_state` is "dirty" here but we won't ever use it again because it is unshrinkable from here. } } self.is_initial_state_shrinkable = false; // Nothing left to do return false; } // This statement should never be reached panic!("Unexpected shrink state"); } /// Find if there's any acceptable included transition that is not current, /// starting from the given index. Expects that all the included transitions /// are currently being rejected (when `can_simplify` returns `false`). fn try_to_find_acceptable_transition(&mut self, ix: usize) -> bool { let mut ix_to_check = ix; loop { if self.included_transitions.test(ix_to_check) && self.check_acceptable( Some(ix_to_check), self.last_valid_initial_state.clone(), ) { self.acceptable_transitions[ix_to_check] = (Accepted, self.transitions[ix_to_check].current()); return true; } // Move on to the next transition if ix_to_check == self.max_ix { ix_to_check = 0; } else { ix_to_check += 1; } // We're back to where we started, there nothing left to do if ix_to_check == ix { return false; } } } /// Check if the sequence of included transitions is acceptable by the /// pre-conditions. When `ix` is not `None`, the transition at the given /// index is taken from its current value. fn check_acceptable(&self, ix: Option, mut state: State) -> bool { let transitions = self.get_included_acceptable_transitions(ix); for transition in transitions.iter() { let is_acceptable = (self.preconditions)(&state, transition); if is_acceptable { state = (self.next)(state, transition); } else { return false; } } true } /// The currently included and acceptable transitions. When `ix` is not /// `None`, the transition at this index is taken from its current value /// which may not be acceptable by the pre-conditions, instead of its /// acceptable value. fn get_included_acceptable_transitions( &self, ix: Option, ) -> Vec { self.acceptable_transitions .iter() .enumerate() // Filter out deleted transitions .filter(|&(this_ix, _)| self.included_transitions.test(this_ix)) // Map the indices to the values .map(|(this_ix, (_, transition))| match ix { Some(ix) if this_ix == ix => self.transitions[ix].current(), _ => transition.clone(), }) .collect() } /// Find if the initial state is still shrinkable or if any of the /// simplifications and complications of the included transitions have not /// yet been rejected. fn can_simplify(&self) -> bool { self.is_initial_state_shrinkable || // If there are some transitions whose shrinking has not yet been // rejected, we can try to shrink them further !self .acceptable_transitions .iter() .enumerate() // Filter out deleted transitions .filter(|&(ix, _)| self.included_transitions.test(ix)) .all(|(_, (state, _transition))| { matches!(state, SimplifyRejected | ComplicateRejected) }) } /// Find the next shrink transition. Loops back to the front of the list /// when the end is reached, because sometimes a transition might become /// acceptable only after a transition that comes before it in the sequence /// gets shrunk. fn next_shrink_transition(&self, current_ix: usize) -> Shrink { if current_ix == self.max_ix { // Either loop back to the start of the list... Transition(0) } else { // ...or move on to the next transition Transition(current_ix + 1) } } } impl< State: Clone + Debug, Transition: Clone + Debug, StateValueTree: ValueTree, TransitionValueTree: ValueTree, > ValueTree for SequentialValueTree< State, Transition, StateValueTree, TransitionValueTree, > { type Value = (State, Vec, Option>); fn current(&self) -> Self::Value { if let Some(seen_transitions_counter) = &self.seen_transitions_counter { if seen_transitions_counter.load(atomic::Ordering::SeqCst) > 0 { panic!("Unexpected non-zero `seen_transitions_counter`"); } } ( self.last_valid_initial_state.clone(), // The current included acceptable transitions self.get_included_acceptable_transitions(None), self.seen_transitions_counter.clone(), ) } fn simplify(&mut self) -> bool { let was_simplified = if self.can_simplify() { self.try_simplify() } else if let Some(Transition(ix)) = self.last_shrink { self.try_to_find_acceptable_transition(ix) } else { false }; // reset seen transactions counter for next run self.seen_transitions_counter = Default::default(); was_simplified } fn complicate(&mut self) -> bool { // reset seen transactions counter for next run self.seen_transitions_counter = Default::default(); match &self.last_shrink { None => false, Some(DeleteTransition(ix)) => { // Undo the last item we deleted. Can't complicate any further, // so unset prev_shrink. self.included_transitions.set(*ix); self.shrinkable_transitions.set(*ix); self.last_shrink = None; true } Some(Transition(ix)) => { let ix = *ix; if self.transitions[ix].complicate() { if self.check_acceptable( Some(ix), self.last_valid_initial_state.clone(), ) { self.acceptable_transitions[ix] = (Accepted, self.transitions[ix].current()); // Don't unset prev_shrink; we may be able to complicate // it again return true; } else { let (state, _trans) = self.acceptable_transitions.get_mut(ix).unwrap(); *state = ComplicateRejected; } } // Can't complicate the last element any further self.last_shrink = None; false } Some(InitialState) => { if self.initial_state.complicate() && self.check_acceptable(None, self.initial_state.current()) { self.last_valid_initial_state = self.initial_state.current(); // Don't unset prev_shrink; we may be able to complicate // it again return true; } // Can't complicate the initial state any further self.last_shrink = None; false } } } } #[cfg(test)] mod test { use super::*; use proptest::collection::hash_set; use proptest::prelude::*; use heap_state_machine::*; use std::collections::HashSet; /// A number of simplifications that can be applied in the `ValueTree` /// produced by [`deterministic_sequential_value_tree`]. It depends on the /// [`TRANSITIONS`] given to its `sequential_strategy`. /// /// This constant can be determined from the test /// `number_of_sequential_value_tree_simplifications`. const SIMPLIFICATIONS: usize = 32; /// Number of transitions in the [`deterministic_sequential_value_tree`]. const TRANSITIONS: usize = 32; #[test] fn number_of_sequential_value_tree_simplifications() { let mut value_tree = deterministic_sequential_value_tree(); value_tree .seen_transitions_counter .as_mut() .unwrap() .store(TRANSITIONS, atomic::Ordering::SeqCst); let mut i = 0; loop { let simplified = value_tree.simplify(); if simplified { i += 1; } else { break; } } assert_eq!(i, SIMPLIFICATIONS); } proptest! { /// Test the simplifications and complication of the /// `SequentialValueTree` produced by /// `deterministic_sequential_value_tree`. /// /// The indices of simplification on which we'll attempt to complicate /// after simplification are selected from the randomly generated /// `complicate_ixs`. /// /// Every simplification and complication must satisfy pre-conditions of /// the state-machine. #[test] fn test_state_machine_sequential_value_tree( complicate_ixs in hash_set(0..SIMPLIFICATIONS, 0..SIMPLIFICATIONS) ) { test_state_machine_sequential_value_tree_aux(complicate_ixs) } } fn test_state_machine_sequential_value_tree_aux( complicate_ixs: HashSet, ) { println!("Complicate indices: {complicate_ixs:?}"); let mut value_tree = deterministic_sequential_value_tree(); let check_preconditions = |value_tree: &TestValueTree| { let (mut state, transitions, _seen_counter) = value_tree.current(); let len = transitions.len(); println!("Transitions {}", len); for (ix, transition) in transitions.into_iter().enumerate() { println!("Transition {}/{len} {transition:?}", ix + 1); // Every transition must satisfy the pre-conditions assert!( ::preconditions( &state, &transition ) ); // Apply the transition to update the state for the next transition state = ::apply( state, &transition, ); } }; let mut ix = 0_usize; loop { let simplified = value_tree.simplify(); check_preconditions(&value_tree); if !simplified { break; } ix += 1; if complicate_ixs.contains(&ix) { loop { let complicated = value_tree.complicate(); check_preconditions(&value_tree); if !complicated { break; } } } } } proptest! { /// Test the initial simplifications of the `SequentialValueTree` produced /// by `deterministic_sequential_value_tree`. /// /// We want to make sure that we initially remove the transitions that /// where not seen. #[test] fn test_value_tree_initial_simplification( len in 10usize..100, ) { test_value_tree_initial_simplification_aux(len) } } fn test_value_tree_initial_simplification_aux(len: usize) { let sequential = ::sequential_strategy( ..len, ); let mut runner = TestRunner::deterministic(); let mut value_tree = sequential.new_tree(&mut runner).unwrap(); let (_, transitions, mut seen_counter) = value_tree.current(); let num_seen = transitions.len() / 2; let seen_counter = seen_counter.as_mut().unwrap(); seen_counter.store(num_seen, atomic::Ordering::SeqCst); let mut seen_before_complication = transitions.into_iter().take(num_seen).collect::>(); assert!(value_tree.simplify()); let (_, transitions, _seen_counter) = value_tree.current(); let seen_after_first_complication = transitions.into_iter().collect::>(); // After the unseen transitions are removed, the shrink behavior depends // on how many transitions were seen: // - If > 1 seen: delete the transition before the last seen one // - If = 1 seen: can't delete any more, may start individual transition shrinking if seen_before_complication.len() > 1 { let last = seen_before_complication.pop().unwrap(); seen_before_complication.pop(); seen_before_complication.push(last); assert_eq!( seen_before_complication, seen_after_first_complication, "only seen transitions should be present after first simplification" ); } else { // When there's only 1 seen transition, we expect it to be preserved. assert!( !seen_after_first_complication.is_empty(), "When only 1 transition was seen, at least 1 should remain after simplification" ); assert!( matches!(value_tree.shrink, Transition(0)), "When only 1 transition was seen, shrink should be set to Transition(0)" ); } } #[test] fn test_call_to_current_with_non_zero_seen_counter() { let result = std::panic::catch_unwind(|| { let value_tree = deterministic_sequential_value_tree(); let (_, _transitions1, mut seen_counter) = value_tree.current(); { let seen_counter = seen_counter.as_mut().unwrap(); seen_counter.store(1, atomic::Ordering::SeqCst); } drop(seen_counter); let _transitions2 = value_tree.current(); }) .expect_err("should panic"); let s = "Unexpected non-zero `seen_transitions_counter`"; assert_eq!(result.downcast_ref::<&str>(), Some(&s)); } /// The following is a definition of an reference state machine used for the /// tests. mod heap_state_machine { use std::vec::Vec; use crate::{ReferenceStateMachine, SequentialValueTree}; use proptest::prelude::*; use proptest::test_runner::TestRunner; use super::TRANSITIONS; pub struct HeapStateMachine; pub type TestValueTree = SequentialValueTree< TestState, TestTransition, as Strategy>::Tree, as Strategy>::Tree, >; pub type TestState = Vec; #[derive(Clone, Debug, PartialEq)] pub enum TestTransition { PopNonEmpty, PopEmpty, Push(i32), } pub fn deterministic_sequential_value_tree() -> TestValueTree { let sequential = ::sequential_strategy( TRANSITIONS, ); let mut runner = TestRunner::deterministic(); sequential.new_tree(&mut runner).unwrap() } impl ReferenceStateMachine for HeapStateMachine { type State = TestState; type Transition = TestTransition; fn init_state() -> BoxedStrategy { Just(vec![]).boxed() } fn transitions( state: &Self::State, ) -> BoxedStrategy { if state.is_empty() { prop_oneof![ 1 => Just(TestTransition::PopEmpty), 2 => (any::()).prop_map(TestTransition::Push), ] .boxed() } else { prop_oneof![ 1 => Just(TestTransition::PopNonEmpty), 2 => (any::()).prop_map(TestTransition::Push), ] .boxed() } } fn apply( mut state: Self::State, transition: &Self::Transition, ) -> Self::State { match transition { TestTransition::PopEmpty => { state.pop(); } TestTransition::PopNonEmpty => { state.pop(); } TestTransition::Push(value) => state.push(*value), } state } fn preconditions( state: &Self::State, transition: &Self::Transition, ) -> bool { match transition { TestTransition::PopEmpty => state.is_empty(), TestTransition::PopNonEmpty => !state.is_empty(), TestTransition::Push(_) => true, } } } } /// A tests that verifies that the strategy finds a simplest failing case, and /// that this simplest failing case is ultimately reported by the test runner, /// as opposed to reporting input that actually passes the test. /// /// This module defines a state machine test that is designed to fail. /// The reference state machine consists of a lower bound the acceptable value /// of a transition. And the test fails if an unacceptably low transition /// value is observed, given the reference state's limit. /// /// This intentionally-failing state machine test is then run inside a proptest /// to verify that it reports a simplest failing input when it fails. mod find_simplest_failure { use proptest::prelude::*; use proptest::strategy::BoxedStrategy; use proptest::test_runner::TestRng; use proptest::{ collection, strategy::Strategy, test_runner::{Config, TestError, TestRunner}, }; use crate::{ReferenceStateMachine, StateMachineTest}; const MIN_TRANSITION: u32 = 10; const MAX_TRANSITION: u32 = 20; const MIN_LIMIT: u32 = 2; const MAX_LIMIT: u32 = 50; #[derive(Debug, Default, Clone)] struct FailIfLessThan(u32); impl ReferenceStateMachine for FailIfLessThan { type State = Self; type Transition = u32; fn init_state() -> BoxedStrategy { (MIN_LIMIT..MAX_LIMIT).prop_map(FailIfLessThan).boxed() } fn transitions(_: &Self::State) -> BoxedStrategy { (MIN_TRANSITION..MAX_TRANSITION).boxed() } fn apply(state: Self::State, _: &Self::Transition) -> Self::State { state } } /// Defines a test that is intended to fail, so that we can inspect the /// failing input. struct FailIfLessThanTest; impl StateMachineTest for FailIfLessThanTest { type SystemUnderTest = (); type Reference = FailIfLessThan; fn init_test(ref_state: &FailIfLessThan) { println!(); println!("starting {ref_state:?}"); } fn apply( (): Self::SystemUnderTest, ref_state: &FailIfLessThan, transition: u32, ) -> Self::SystemUnderTest { // Fail on any transition that is less than the ref state's limit. let FailIfLessThan(limit) = ref_state; println!("{transition} < {}?", limit); if transition < ref_state.0 { panic!("{transition} < {}", limit); } } } proptest! { #[test] fn test_returns_simplest_failure( seed in collection::vec(any::(), 32).no_shrink()) { // We need to explicitly run create a runner so that we can // inspect the output, and determine if it does return an input that // should fail, and is minimal. let mut runner = TestRunner::new_with_rng( Config::default(), TestRng::from_seed(Default::default(), &seed)); let result = runner.run( &FailIfLessThan::sequential_strategy(10..50_usize), |(ref_state, transitions, seen_counter)| { Ok(FailIfLessThanTest::test_sequential( Default::default(), ref_state, transitions, seen_counter, )) }, ); if let Err(TestError::Fail( _, (FailIfLessThan(limit), transitions, _seen_counter), )) = result { assert_eq!(transitions.len(), 1, "The minimal failing case should be "); assert_eq!(limit, MIN_TRANSITION + 1); assert!(transitions.into_iter().next().unwrap() < limit); } else { prop_assume!(false, "If the state machine doesn't fail as intended, we need a case that fails."); } } } } #[test] fn test_zero_seen_transitions_optimization() { // Test that when 0 transitions are seen, we go directly to InitialState shrinking let mut value_tree = deterministic_sequential_value_tree(); // Simulate that no transitions were seen (kept_count = 0) value_tree .seen_transitions_counter .as_mut() .unwrap() .store(0, atomic::Ordering::SeqCst); // Call simplify - this should trigger the optimization let simplified = value_tree.simplify(); assert_eq!(value_tree.included_transitions.count(), 0, "All transitions should be removed when none were seen"); assert!(matches!(value_tree.shrink, InitialState), "Shrink should be set to InitialState when kept_count == 0"); // The HeapStateMachine uses Just(vec![]) for initial state, which is not shrinkable // So simplify() should return false, but the optimization still works correctly assert!(!simplified, "Simplification should return false since initial state (Just(vec![])) is not shrinkable"); let (_, transitions, _) = value_tree.current(); assert!(transitions.is_empty(), "No transitions should remain when none were seen"); } } proptest-state-machine-0.4.0/src/test_runner.rs000064400000000000000000000232611046102023000177360ustar 00000000000000//- // Copyright 2023 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Test declaration helpers and runners for abstract state machine testing. use std::sync::atomic::{self, AtomicUsize}; use std::sync::Arc; use crate::strategy::ReferenceStateMachine; use proptest::test_runner::Config; /// State machine test that relies on a reference state machine model pub trait StateMachineTest { /// The concrete state, that is the system under test (SUT). type SystemUnderTest; /// The abstract state machine that implements [`ReferenceStateMachine`] /// drives the generation of the state machine's transitions. type Reference: ReferenceStateMachine; /// Initialize the state of SUT. /// /// If the reference state machine is generated from a non-constant /// strategy, ensure to use it to initialize the SUT to a corresponding /// state. fn init_test( ref_state: &::State, ) -> Self::SystemUnderTest; /// Apply a transition in the SUT state and check post-conditions. /// The post-conditions are properties of your state machine that you want /// to assert. /// /// Note that the `ref_state` is the state *after* this `transition` is /// applied. You can use it to compare it with your SUT after you apply /// the transition. fn apply( state: Self::SystemUnderTest, ref_state: &::State, transition: ::Transition, ) -> Self::SystemUnderTest; /// Check some invariant on the SUT state after every transition. /// /// Note that just like in [`StateMachineTest::apply`] you can use /// the `ref_state` to compare it with your SUT. fn check_invariants( state: &Self::SystemUnderTest, ref_state: &::State, ) { // This is to avoid `unused_variables` warning let _ = (state, ref_state); } /// Override this function to add some teardown logic on the SUT state /// at the end of each test case. The default implementation simply drops /// the state. fn teardown(state: Self::SystemUnderTest) { // This is to avoid `unused_variables` warning let _ = state; } /// Run the test sequentially. You typically don't need to override this /// method. fn test_sequential( config: Config, mut ref_state: ::State, transitions: Vec< ::Transition, >, mut seen_counter: Option>, ) { #[cfg(feature = "std")] use proptest::test_runner::INFO_LOG; let trans_len = transitions.len(); #[cfg(feature = "std")] if config.verbose >= INFO_LOG { eprintln!(); eprintln!("Running a test case with {} transitions.", trans_len); } #[cfg(not(feature = "std"))] let _ = (config, trans_len); let mut concrete_state = Self::init_test(&ref_state); // Check the invariants on the initial state Self::check_invariants(&concrete_state, &ref_state); for (ix, transition) in transitions.into_iter().enumerate() { // The counter is `Some` only before shrinking. When it's `Some` it // must be incremented before every transition that's being applied // to inform the strategy that the transition has been applied for // the first step of its shrinking process which removes any unseen // transitions. if let Some(seen_counter) = seen_counter.as_mut() { seen_counter.fetch_add(1, atomic::Ordering::SeqCst); } #[cfg(feature = "std")] if config.verbose >= INFO_LOG { eprintln!(); eprintln!( "Applying transition {}/{}: {:?}", ix + 1, trans_len, transition ); } #[cfg(not(feature = "std"))] let _ = ix; // Apply the transition on the states ref_state = ::apply( ref_state, &transition, ); concrete_state = Self::apply(concrete_state, &ref_state, transition); // Check the invariants after the transition is applied Self::check_invariants(&concrete_state, &ref_state); } Self::teardown(concrete_state) } } /// This macro helps to turn a state machine test implementation into a runnable /// test. The macro expects a function header whose arguments follow a special /// syntax rules: First, we declare if we want to apply the state machine /// transitions sequentially or concurrently (currently, only the `sequential` /// is supported). Next, we give a range of how many transitions to generate, /// followed by `=>` and finally, an identifier that must implement /// `StateMachineTest`. /// /// ## Example /// /// ```rust,ignore /// struct MyTest; /// /// impl StateMachineTest for MyTest {} /// /// prop_state_machine! { /// #[test] /// fn run_with_macro(sequential 1..20 => MyTest); /// } /// ``` /// /// This example will expand to: /// /// ```rust,ignore /// struct MyTest; /// /// impl StateMachineTest for MyTest {} /// /// proptest! { /// #[test] /// fn run_with_macro( /// (initial_state, transitions) in MyTest::sequential_strategy(1..20) /// ) { /// MyTest::test_sequential(initial_state, transitions) /// } /// } /// ``` #[macro_export] macro_rules! prop_state_machine { // With proptest config annotation (#![proptest_config($config:expr)] $( $(#[$meta:meta])* fn $test_name:ident(sequential $size:expr => $test:ident $(< $( $ty_param:tt ),+ >)?); )*) => { $( ::proptest::proptest! { #![proptest_config($config)] $(#[$meta])* fn $test_name( (initial_state, transitions, seen_counter) in <<$test $(< $( $ty_param ),+ >)? as $crate::StateMachineTest>::Reference as $crate::ReferenceStateMachine>::sequential_strategy($size) ) { let config = $config.__sugar_to_owned(); <$test $(::< $( $ty_param ),+ >)? as $crate::StateMachineTest>::test_sequential(config, initial_state, transitions, seen_counter) } } )* }; // Without proptest config annotation ($( $(#[$meta:meta])* fn $test_name:ident(sequential $size:expr => $test:ident $(< $( $ty_param:tt ),+ >)?); )*) => { $( ::proptest::proptest! { $(#[$meta])* fn $test_name( (initial_state, transitions, seen_counter) in <<$test $(< $( $ty_param ),+ >)? as $crate::StateMachineTest>::Reference as $crate::ReferenceStateMachine>::sequential_strategy($size) ) { <$test $(::< $( $ty_param ),+ >)? as $crate::StateMachineTest>::test_sequential( ::proptest::test_runner::Config::default(), initial_state, transitions, seen_counter) } } )* }; } #[cfg(test)] mod tests { mod macro_test { //! tests to verify that invocations of all forms of the //! `prop_state_machine!` macro compile cleanly, and hygenically, //! as intended. /// Note: no imports here, so as to guarantee hygienic macros /// A no-op test. Exists strictly as something to reference /// in the macro invocation. struct Test; impl crate::ReferenceStateMachine for Test { type State = (); type Transition = (); fn init_state() -> proptest::strategy::BoxedStrategy { use proptest::prelude::*; Just(()).boxed() } fn transitions( _: &Self::State, ) -> proptest::strategy::BoxedStrategy { use proptest::prelude::*; Just(()).boxed() } fn apply(_: Self::State, _: &Self::Transition) -> Self::State { () } } impl crate::StateMachineTest for Test { type SystemUnderTest = (); type Reference = Self; fn init_test( _: &::State, ) -> Self::SystemUnderTest { } fn apply( _: Self::SystemUnderTest, _: &::State, _: ::Transition, ) -> Self::SystemUnderTest { } } // Invocation of the `prop_state_machine` macro without // a `![proptest_config]` annotation prop_state_machine! { #[test] fn no_config_annotation(sequential 1..2 => Test); } // Invocation of the `prop_state_machine` macro with a // `![proptest_config]` annotation prop_state_machine! { #![proptest_config(::proptest::test_runner::Config::default())] #[test] fn with_config_annotation(sequential 1..2 => Test); } } }