├── Cargo.toml ├── img └── honggfuzz.png ├── rustfmt.toml ├── .cargo └── config.toml ├── .gitignore ├── proc_macro ├── Cargo.toml ├── LICENSE └── src │ └── lib.rs ├── lib ├── Cargo.toml ├── src │ └── lib.rs ├── LICENSE └── README.md ├── examples ├── Cargo.toml ├── src │ ├── vec.rs │ ├── borrow.rs │ ├── binary_heap.rs │ ├── url.rs │ ├── linked_hash_map.rs │ ├── btree_map.rs │ ├── index_map.rs │ └── hash_map.rs └── LICENSE ├── .github └── workflows │ └── rust.yml ├── README.md └── DEBUGGING.md /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "lib", 4 | "examples" 5 | ] 6 | -------------------------------------------------------------------------------- /img/honggfuzz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubadamw/rutenspitz/HEAD/img/honggfuzz.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | overflow_delimited_expr = true 2 | reorder_impl_items = true 3 | unstable_features = true 4 | use_field_init_shorthand = true 5 | version = "Two" 6 | wrap_comments = true 7 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all())'] 2 | rustflags = [ 3 | "-Dclippy::all", 4 | "-Dclippy::panic", 5 | "-Dclippy::pedantic", 6 | "-Dnonstandard-style", 7 | "-Drust-2018-idioms", 8 | "-Aclippy::ptr-arg", 9 | ] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # Honggfuzz 13 | hfuzz_target 14 | hfuzz_workspace 15 | -------------------------------------------------------------------------------- /proc_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rutenspitz_macro" 3 | description = "А procedural macro to be used for testing/fuzzing stateful models against a semantically equivalent but obviously correct implementation" 4 | version = "0.2.1" 5 | authors = ["Jakub Wieczorek "] 6 | edition = "2021" 7 | license = "MIT" 8 | keywords = ["fuzzing", "test", "honggfuzz"] 9 | repository = "https://github.com/jakubadamw/rutenspitz" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | either = "1" 16 | proc-macro2 = "1" 17 | quote = "1" 18 | syn = { version = "2", features = ["full"] } 19 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rutenspitz" 3 | description = "А procedural macro to be used for testing/fuzzing stateful models against a semantically equivalent but obviously correct implementation" 4 | version = "0.2.1" 5 | authors = ["Jakub Wieczorek "] 6 | edition = "2021" 7 | license = "MIT" 8 | keywords = ["fuzzing", "test", "honggfuzz"] 9 | repository = "https://github.com/jakubadamw/rutenspitz" 10 | 11 | [dependencies] 12 | arbitrary = { version = "1", features = ["derive"] } 13 | lazy_static = "1" 14 | rutenspitz_macro = { version = "0.2", path = "../proc_macro" } 15 | strum_macros = "0.24" 16 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use rutenspitz_macro::arbitrary_stateful_operations; 2 | 3 | lazy_static::lazy_static! { 4 | pub static ref NON_DEBUG_PANIC_HOOK: () = { 5 | std::panic::set_hook(Box::new(|panic_info| { 6 | if panic_info.payload().is::() { 7 | std::process::abort(); 8 | } 9 | })); 10 | }; 11 | } 12 | 13 | #[macro_export] 14 | macro_rules! panic { 15 | ($($arg:tt)*) => { std::panic::panic_any(rutenspitz::OutcomePanic(format!($($arg)*))) }; 16 | } 17 | 18 | pub struct OutcomePanic(pub String); 19 | 20 | pub use lazy_static; 21 | 22 | pub mod derive { 23 | pub use arbitrary::Arbitrary; 24 | pub use strum_macros::IntoStaticStr; 25 | } 26 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rutenspitz-examples" 3 | version = "0.1.2" 4 | authors = ["Jakub Wieczorek "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [[bin]] 9 | name = "binary_heap" 10 | path = "src/binary_heap.rs" 11 | 12 | [[bin]] 13 | name = "btree_map" 14 | path = "src/btree_map.rs" 15 | 16 | [[bin]] 17 | name = "hash_map" 18 | path = "src/hash_map.rs" 19 | 20 | [[bin]] 21 | name = "index_map" 22 | path = "src/index_map.rs" 23 | 24 | [[bin]] 25 | name = "linked_hash_map" 26 | path = "src/linked_hash_map.rs" 27 | 28 | [[bin]] 29 | name = "url" 30 | path = "src/url.rs" 31 | 32 | [[bin]] 33 | name = "vec" 34 | path = "src/vec.rs" 35 | 36 | [[bin]] 37 | name = "borrow" 38 | path = "src/borrow.rs" 39 | 40 | [dependencies] 41 | ahash = "0.8" 42 | arbitrary = "1" 43 | better-panic = "0.3" 44 | hashbrown = "0.13" 45 | honggfuzz = "0.5" 46 | indexmap = "1" 47 | linked-hash-map = "0.5" 48 | rutenspitz = { path = "../lib" } 49 | url = "2" 50 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | toolchain: [nightly] 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: ${{ matrix.toolchain }} 18 | override: true 19 | components: clippy 20 | - name: Install Honggfuzz dependencies 21 | run: sudo apt install build-essential binutils-dev libunwind-dev libblocksruntime-dev liblzma-dev 22 | - name: Install Honggfuzz 23 | run: cargo install honggfuzz 24 | - name: Build 25 | run: cargo build --lib --verbose 26 | - name: Build Honggfuzz targets 27 | run: cargo hfuzz build --verbose 28 | - name: Build Honggfuzz targets in debug mode 29 | run: cargo hfuzz build-debug --verbose 30 | - name: Run clippy 31 | run: cargo clippy --all-targets --all-features -- -D warnings -D clippy::pedantic 32 | -------------------------------------------------------------------------------- /examples/src/vec.rs: -------------------------------------------------------------------------------- 1 | //#![allow(clippy::let_unit_value)] 2 | use honggfuzz::fuzz; 3 | use rutenspitz::arbitrary_stateful_operations; 4 | 5 | arbitrary_stateful_operations! { 6 | model = Vec, 7 | tested = Vec, 8 | 9 | type_parameters = , 10 | 11 | methods { 12 | equal { 13 | fn extend_from_slice(&mut self, sli: &[T]); 14 | } 15 | } 16 | } 17 | 18 | #[allow(clippy::unnecessary_wraps)] 19 | fn fuzz_cycle(data: &[u8]) -> arbitrary::Result<()> { 20 | use arbitrary::{Arbitrary, Unstructured}; 21 | 22 | let mut ring = Unstructured::new(data); 23 | 24 | let mut tested = Vec::::new(); 25 | 26 | let mut op_trace = String::new(); 27 | while let Ok(op) = as Arbitrary>::arbitrary(&mut ring) { 28 | op.append_to_trace(&mut op_trace); 29 | op.execute(&mut tested); 30 | } 31 | 32 | Ok(()) 33 | } 34 | 35 | fn main() -> Result<(), ()> { 36 | better_panic::install(); 37 | 38 | loop { 39 | fuzz!(|data: &[u8]| { 40 | let _ = fuzz_cycle(data); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019–2020 Jakub Wieczorek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019–2020 Jakub Wieczorek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /proc_macro/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019–2020 Jakub Wieczorek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/src/borrow.rs: -------------------------------------------------------------------------------- 1 | use honggfuzz::fuzz; 2 | use rutenspitz::arbitrary_stateful_operations; 3 | 4 | pub struct Extender<'a, T>(&'a mut Vec); 5 | 6 | impl<'a, T: Clone> Extender<'a, T> { 7 | fn extend_from_slice(&mut self, slice: &[T]) { 8 | self.0.extend_from_slice(slice); 9 | } 10 | } 11 | 12 | arbitrary_stateful_operations! { 13 | model = Extender<'a, T>, 14 | tested = Extender<'a, T>, 15 | 16 | type_parameters = <'a, T: Clone + std::fmt::Debug>, 17 | 18 | methods { 19 | equal { 20 | fn extend_from_slice(&mut self, sli: &[T]); 21 | } 22 | } 23 | } 24 | 25 | #[allow(clippy::unnecessary_wraps)] 26 | fn fuzz_cycle(data: &[u8]) -> arbitrary::Result<()> { 27 | use arbitrary::{Arbitrary, Unstructured}; 28 | 29 | let mut ring = Unstructured::new(data); 30 | 31 | let mut vec = Vec::::new(); 32 | let mut tested = Extender(&mut vec); 33 | 34 | let mut op_trace = String::new(); 35 | while let Ok(op) = as Arbitrary>::arbitrary(&mut ring) { 36 | op.append_to_trace(&mut op_trace); 37 | op.execute(&mut tested); 38 | } 39 | 40 | Ok(()) 41 | } 42 | 43 | fn main() -> Result<(), ()> { 44 | better_panic::install(); 45 | 46 | loop { 47 | fuzz!(|data: &[u8]| { 48 | let _ = fuzz_cycle(data); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/src/binary_heap.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::must_use_candidate)] 2 | 3 | use honggfuzz::fuzz; 4 | use rutenspitz::arbitrary_stateful_operations; 5 | 6 | use std::collections::BinaryHeap; 7 | use std::fmt::Debug; 8 | 9 | #[derive(Default)] 10 | pub struct ModelBinaryHeap 11 | where 12 | T: Ord, 13 | { 14 | data: Vec, 15 | } 16 | 17 | impl ModelBinaryHeap 18 | where 19 | T: Ord, 20 | { 21 | pub fn clear(&mut self) { 22 | self.data.clear(); 23 | } 24 | 25 | pub fn is_empty(&self) -> bool { 26 | self.data.is_empty() 27 | } 28 | 29 | pub fn len(&self) -> usize { 30 | self.data.len() 31 | } 32 | 33 | pub fn peek(&self) -> Option<&T> { 34 | self.data.iter().max() 35 | } 36 | 37 | pub fn pop(&mut self) -> Option { 38 | let max = self 39 | .data 40 | .iter() 41 | .enumerate() 42 | .max_by_key(|probe| probe.1) 43 | .map(|probe| probe.0); 44 | max.map(|idx| self.data.swap_remove(idx)) 45 | } 46 | 47 | pub fn push(&mut self, item: T) { 48 | self.data.push(item); 49 | } 50 | 51 | pub fn drain(&mut self) -> impl Iterator + '_ { 52 | self.data.drain(..) 53 | } 54 | } 55 | 56 | fn sort_iterator>(i: I) -> Vec { 57 | let mut v: Vec<_> = i.collect::>(); 58 | v.sort(); 59 | v 60 | } 61 | 62 | arbitrary_stateful_operations! { 63 | model = ModelBinaryHeap, 64 | tested = BinaryHeap, 65 | 66 | type_parameters = < 67 | T: Clone + Debug + Eq + Ord 68 | >, 69 | 70 | methods { 71 | equal { 72 | fn clear(&mut self); 73 | fn is_empty(&self) -> bool; 74 | fn len(&self) -> usize; 75 | fn peek(&self) -> Option<&T>; 76 | fn push(&mut self, item: T); 77 | } 78 | 79 | equal_with(sort_iterator) { 80 | fn drain(&mut self) -> impl Iterator; 81 | } 82 | } 83 | } 84 | 85 | fn fuzz_cycle(data: &[u8]) -> arbitrary::Result<()> { 86 | use arbitrary::{Arbitrary, Unstructured}; 87 | 88 | let mut ring = Unstructured::new(data); 89 | let capacity: u8 = Arbitrary::arbitrary(&mut ring)?; 90 | 91 | let mut model = ModelBinaryHeap::::default(); 92 | let mut tested = BinaryHeap::::with_capacity(capacity as usize); 93 | 94 | let mut op_trace = String::new(); 95 | while let Ok(op) = as Arbitrary>::arbitrary(&mut ring) { 96 | op.append_to_trace(&mut op_trace); 97 | op.execute_and_compare(&mut model, &mut tested); 98 | } 99 | 100 | Ok(()) 101 | } 102 | 103 | fn main() -> Result<(), ()> { 104 | better_panic::install(); 105 | 106 | loop { 107 | fuzz!(|data: &[u8]| { 108 | let _ = fuzz_cycle(data); 109 | }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rutenspitz 2 | 3 | **NOTE**: *This crate was previously called `arbitrary-model-tests`.* 4 | 5 | [![Build status](https://github.com/jakubadamw/rutenspitz/workflows/Build/badge.svg)](https://github.com/jakubadamw/rutenspitz/actions?query=workflow%3ABuild) 6 | [![crates.io](https://img.shields.io/crates/v/rutenspitz.svg)](https://crates.io/crates/rutenspitz) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | This is an attempt at creating a convenient procedural macro to be used for testing stateful models (in particular, various kinds of data structures) against a trivial (but usually very inefficient) implementation that is semantically 100% equivalent to the target implementation but, in contrast, *obviously* correct. The purpose of the macro is to generate the boilerplate code for testing particular operations of the model so that the user-provided definition of the test for a given stateful structure becomes as succinct as possible. 10 | 11 | This crate was inspired by the following works: 12 | 13 | * [bughunt-rust](https://github.com/blt/bughunt-rust) 14 | * [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) 15 | * [honggfuzz-rs](https://github.com/rust-fuzz/honggfuzz-rs) 16 | 17 | ## Example 18 | 19 | See the [`HashMap` test](examples/src/hash_map.rs) for reference. 20 | 21 | You can run it with `cargo hfuzz`. First of all you'll need to install `honggfuzz` along with its system dependencies. See [this section](https://github.com/rust-fuzz/honggfuzz-rs#dependencies) for more details. When you're done, all you need to run the test: 22 | 23 | ``` 24 | cargo hfuzz run hash_map 25 | ``` 26 | 27 | ## DSL 28 | 29 | This is the initial take at a DSL that describes the stateful model to be tested (`std::collections::HashMap` in this case). 30 | 31 | ```rust 32 | arbitrary_stateful_operations! { 33 | model = ModelHashMap, 34 | tested = HashMap, 35 | 36 | type_parameters = < 37 | K: Clone + Debug + Eq + Hash + Ord, 38 | V: Clone + Debug + Eq + Ord 39 | >, 40 | 41 | methods { 42 | equal { 43 | fn clear(&mut self); 44 | fn contains_key(&self, k: &K) -> bool; 45 | fn get(&self, k: &K) -> Option<&V>; 46 | fn get_key_value(&self, k: &K) -> Option<(&K, &V)>; 47 | fn get_mut(&mut self, k: &K) -> Option<&mut V>; 48 | fn insert(&mut self, k: K, v: V) -> Option; 49 | // Tested as invariants, so no longer needed. 50 | // fn is_empty(&self) -> bool; 51 | // fn len(&self) -> usize; 52 | fn remove(&mut self, k: &K) -> Option; 53 | } 54 | 55 | equal_with(sort_iterator) { 56 | fn drain(&mut self) -> impl Iterator; 57 | fn iter(&self) -> impl Iterator; 58 | fn iter_mut(&self) -> impl Iterator; 59 | fn keys(&self) -> impl Iterator; 60 | fn values(&self) -> impl Iterator; 61 | fn values_mut(&mut self) -> impl Iterator; 62 | } 63 | } 64 | 65 | pre { 66 | let prev_capacity = tested.capacity(); 67 | } 68 | 69 | post { 70 | if op_name == "clear" { 71 | assert_eq!(tested.capacity(), prev_capacity); 72 | } 73 | 74 | assert!(tested.capacity() >= model.len()); 75 | assert_eq!(tested.is_empty(), model.is_empty()); 76 | assert_eq!(tested.len(), model.len()); 77 | } 78 | } 79 | ``` 80 | 81 | ## Debugging 82 | 83 | See [this guide](DEBUGGING.md). 84 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # rutenspitz 2 | 3 | **NOTE**: *This crate was previously called `arbitrary-model-tests`.* 4 | 5 | [![Build status](https://github.com/jakubadamw/rutenspitz/workflows/Build/badge.svg)](https://github.com/jakubadamw/rutenspitz/actions?query=workflow%3ABuild) 6 | [![crates.io](https://img.shields.io/crates/v/rutenspitz.svg)](https://crates.io/crates/rutenspitz) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | This is an attempt at creating a convenient procedural macro to be used for testing stateful models (in particular, various kinds of data structures) against a trivial (but usually very inefficient) implementation that is semantically 100% equivalent to the target implementation but, in contrast, *obviously* correct. The purpose of the macro is to generate the boilerplate code for testing particular operations of the model so that the user-provided definition of the test for a given stateful structure becomes as succinct as possible. 10 | 11 | This crate was inspired by the following works: 12 | 13 | * [bughunt-rust](https://github.com/blt/bughunt-rust) 14 | * [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) 15 | * [honggfuzz-rs](https://github.com/rust-fuzz/honggfuzz-rs) 16 | 17 | ## Example 18 | 19 | See the [`HashMap` test](../examples/src/hash_map.rs) for reference. 20 | 21 | You can run it with `cargo hfuzz`. First of all you'll need to install `honggfuzz` along with its system dependencies. See [this section](https://github.com/rust-fuzz/honggfuzz-rs#dependencies) for more details. When you're done, all you need to run the test: 22 | 23 | ``` 24 | cargo hfuzz run hash_map 25 | ``` 26 | 27 | ## DSL 28 | 29 | This is the initial take at a DSL that describes the stateful model to be tested (`std::collections::HashMap` in this case). 30 | 31 | ```rust 32 | arbitrary_stateful_operations! { 33 | model = ModelHashMap, 34 | tested = HashMap, 35 | 36 | type_parameters = < 37 | K: Clone + Debug + Eq + Hash + Ord, 38 | V: Clone + Debug + Eq + Ord 39 | >, 40 | 41 | methods { 42 | equal { 43 | fn clear(&mut self); 44 | fn contains_key(&self, k: &K) -> bool; 45 | fn get(&self, k: &K) -> Option<&V>; 46 | fn get_key_value(&self, k: &K) -> Option<(&K, &V)>; 47 | fn get_mut(&mut self, k: &K) -> Option<&mut V>; 48 | fn insert(&mut self, k: K, v: V) -> Option; 49 | // Tested as invariants, so no longer needed. 50 | // fn is_empty(&self) -> bool; 51 | // fn len(&self) -> usize; 52 | fn remove(&mut self, k: &K) -> Option; 53 | } 54 | 55 | equal_with(sort_iterator) { 56 | fn drain(&mut self) -> impl Iterator; 57 | fn iter(&self) -> impl Iterator; 58 | fn iter_mut(&self) -> impl Iterator; 59 | fn keys(&self) -> impl Iterator; 60 | fn values(&self) -> impl Iterator; 61 | fn values_mut(&mut self) -> impl Iterator; 62 | } 63 | } 64 | 65 | pre { 66 | let prev_capacity = tested.capacity(); 67 | } 68 | 69 | post { 70 | if op_name == "clear" { 71 | assert_eq!(tested.capacity(), prev_capacity); 72 | } 73 | 74 | assert!(tested.capacity() >= model.len()); 75 | assert_eq!(tested.is_empty(), model.is_empty()); 76 | assert_eq!(tested.len(), model.len()); 77 | } 78 | } 79 | ``` 80 | 81 | ## Debugging 82 | 83 | See [this guide](../DEBUGGING.md). 84 | -------------------------------------------------------------------------------- /examples/src/url.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::let_unit_value)] 2 | 3 | use honggfuzz::fuzz; 4 | use rutenspitz::arbitrary_stateful_operations; 5 | 6 | use std::fmt::Debug; 7 | 8 | trait UrlFix { 9 | fn set_fragment_(&mut self, fragment: &Option); 10 | fn set_host_(&mut self, host: &Option); 11 | fn set_password_(&mut self, password: &Option); 12 | fn set_query_(&mut self, query: &Option); 13 | } 14 | 15 | impl UrlFix for url::Url { 16 | fn set_fragment_(&mut self, fragment: &Option) { 17 | self.set_fragment(fragment.as_ref().map(String::as_str)); 18 | } 19 | 20 | fn set_host_(&mut self, host: &Option) { 21 | let _ = self.set_host(host.as_ref().map(String::as_str)); 22 | } 23 | 24 | fn set_password_(&mut self, password: &Option) { 25 | let _ = self.set_password(password.as_ref().map(String::as_str)); 26 | } 27 | 28 | fn set_query_(&mut self, query: &Option) { 29 | let _ = self.set_query(query.as_ref().map(String::as_str)); 30 | } 31 | } 32 | 33 | #[allow(dead_code)] 34 | fn map_to_vec>(i: Option) -> Option> { 35 | i.map(Iterator::collect) 36 | } 37 | 38 | arbitrary_stateful_operations! { 39 | model = url::Url, 40 | tested = url::Url, 41 | 42 | type_parameters = <>, 43 | 44 | methods { 45 | equal { 46 | fn as_str(&self) -> &str; 47 | fn cannot_be_a_base(&self) -> bool; 48 | fn domain(&self) -> Option<&str>; 49 | fn fragment(&self) -> Option<&fragment>; 50 | fn has_authority(&self) -> bool; 51 | fn has_host(&self) -> bool; 52 | fn host(&self) -> Option>; 53 | fn host_str(&self) -> Option<&str>; 54 | fn join(&self, input: &String) -> Result; 55 | fn origin(&self) -> url::Origin; 56 | fn password(&self) -> Option<&str>; 57 | fn path(&self) -> &str; 58 | fn port(&self) -> Option; 59 | fn port_or_known_default(&self) -> Option; 60 | fn query(&self) -> Option<&str>; 61 | fn scheme(&self) -> &str; 62 | fn set_fragment_(&mut self, fragment: &Option); 63 | fn set_host_(&mut self, host: &Option); 64 | fn set_password_(&mut self, password: &Option); 65 | fn set_path(&mut self, path: &String); 66 | fn set_port(&mut self, port: Option); 67 | fn set_query_(&mut self, query: &Option); 68 | fn set_scheme(&mut self, scheme: &String); 69 | fn set_username(&mut self, username: &String); 70 | fn to_file_path(&self) -> Result; 71 | fn username(&self) -> &str; 72 | } 73 | 74 | equal_with(std::iter::Iterator::collect::>) { 75 | fn query_pairs(&self) -> url::Parse; 76 | } 77 | 78 | equal_with(map_to_vec) { 79 | fn path_segments(&self) -> Option>; 80 | } 81 | } 82 | } 83 | 84 | #[allow(clippy::unnecessary_wraps)] 85 | fn fuzz_cycle(data: &[u8]) -> arbitrary::Result<()> { 86 | use arbitrary::{Arbitrary, Unstructured}; 87 | 88 | let mut ring = Unstructured::new(data); 89 | 90 | let mut tested = url::Url::parse("https://example.org").unwrap(); 91 | 92 | let mut op_trace = String::new(); 93 | while let Ok(op) = ::arbitrary(&mut ring) { 94 | op.append_to_trace(&mut op_trace); 95 | op.execute(&mut tested); 96 | } 97 | 98 | Ok(()) 99 | } 100 | 101 | fn main() -> Result<(), ()> { 102 | better_panic::install(); 103 | 104 | loop { 105 | fuzz!(|data: &[u8]| { 106 | let _ = fuzz_cycle(data); 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /examples/src/linked_hash_map.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::manual_find_map)] 2 | #![allow(clippy::must_use_candidate)] 3 | 4 | use honggfuzz::fuzz; 5 | use rutenspitz::arbitrary_stateful_operations; 6 | 7 | use linked_hash_map::LinkedHashMap; 8 | 9 | use std::fmt::Debug; 10 | use std::hash::Hash; 11 | 12 | #[derive(Default)] 13 | pub struct ModelHashMap 14 | where 15 | K: Eq + Hash, 16 | { 17 | data: Vec<(K, V)>, 18 | } 19 | 20 | impl ModelHashMap 21 | where 22 | K: Eq + Hash, 23 | { 24 | pub fn clear(&mut self) { 25 | self.data.clear(); 26 | } 27 | 28 | pub fn contains_key(&self, k: &K) -> bool { 29 | self.data.iter().any(|probe| probe.0 == *k) 30 | } 31 | 32 | pub fn get(&self, k: &K) -> Option<&V> { 33 | self.data.iter().find(|probe| probe.0 == *k).map(|e| &e.1) 34 | } 35 | 36 | pub fn get_mut(&mut self, k: &K) -> Option<&mut V> { 37 | self.data 38 | .iter_mut() 39 | .find(|probe| probe.0 == *k) 40 | .map(|e| &mut e.1) 41 | } 42 | 43 | pub fn insert(&mut self, k: K, v: V) -> Option { 44 | let old_value = self.remove(&k); 45 | 46 | self.data.push((k, v)); 47 | old_value 48 | } 49 | 50 | pub fn is_empty(&self) -> bool { 51 | self.data.is_empty() 52 | } 53 | 54 | pub fn len(&self) -> usize { 55 | self.data.len() 56 | } 57 | 58 | #[allow(clippy::missing_panics_doc)] 59 | pub fn remove(&mut self, k: &K) -> Option { 60 | let pos = self.data.iter().position(|probe| probe.0 == *k); 61 | pos.map(|idx| { 62 | let mut rest = self.data.split_off(idx); 63 | let mut it = rest.drain(..); 64 | let el = it.next().unwrap().1; 65 | self.data.extend(it); 66 | el 67 | }) 68 | } 69 | 70 | pub fn iter(&self) -> impl Iterator { 71 | self.data.iter().map(|e| (&e.0, &e.1)) 72 | } 73 | 74 | pub fn iter_mut(&mut self) -> impl Iterator { 75 | self.data.iter_mut().map(|e| (&e.0, &mut e.1)) 76 | } 77 | 78 | pub fn keys(&self) -> impl Iterator { 79 | self.data.iter().map(|e| &e.0) 80 | } 81 | 82 | pub fn values(&self) -> impl Iterator { 83 | self.data.iter().map(|e| &e.1) 84 | } 85 | } 86 | 87 | fn collect_iterator>(i: I) -> Vec { 88 | i.collect() 89 | } 90 | 91 | arbitrary_stateful_operations! { 92 | model = ModelHashMap, 93 | tested = LinkedHashMap, 94 | 95 | type_parameters = < 96 | K: Clone + Debug + Eq + Hash + Ord, 97 | V: Clone + Debug + Eq + Ord 98 | >, 99 | 100 | methods { 101 | equal { 102 | fn clear(&mut self); 103 | fn contains_key(&self, k: &K) -> bool; 104 | fn get(&self, k: &K) -> Option<&V>; 105 | fn get_mut(&mut self, k: &K) -> Option<&mut V>; 106 | fn insert(&mut self, k: K, v: V) -> Option; 107 | fn is_empty(&self) -> bool; 108 | fn len(&self) -> usize; 109 | fn remove(&mut self, k: &K) -> Option; 110 | } 111 | 112 | equal_with(collect_iterator) { 113 | fn iter(&self) -> impl Iterator; 114 | fn iter_mut(&self) -> impl Iterator; 115 | fn keys(&self) -> impl Iterator; 116 | fn values(&self) -> impl Iterator; 117 | } 118 | } 119 | } 120 | 121 | fn fuzz_cycle(data: &[u8]) -> arbitrary::Result<()> { 122 | use arbitrary::{Arbitrary, Unstructured}; 123 | 124 | let mut ring = Unstructured::new(data); 125 | let capacity: u8 = Arbitrary::arbitrary(&mut ring)?; 126 | 127 | let mut model = ModelHashMap::::default(); 128 | let mut tested = LinkedHashMap::::with_capacity(capacity as usize); 129 | 130 | let mut op_trace = String::new(); 131 | while let Ok(op) = as Arbitrary>::arbitrary(&mut ring) { 132 | op.append_to_trace(&mut op_trace); 133 | op.execute_and_compare(&mut model, &mut tested); 134 | } 135 | 136 | Ok(()) 137 | } 138 | 139 | fn main() -> Result<(), ()> { 140 | better_panic::install(); 141 | 142 | loop { 143 | fuzz!(|data: &[u8]| { 144 | let _ = fuzz_cycle(data); 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /examples/src/btree_map.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::manual_filter_map)] 2 | #![allow(clippy::manual_find_map)] 3 | #![allow(clippy::must_use_candidate)] 4 | #![allow(clippy::option_if_let_else)] 5 | 6 | use honggfuzz::fuzz; 7 | use rutenspitz::arbitrary_stateful_operations; 8 | 9 | use std::collections::BTreeMap; 10 | use std::fmt::Debug; 11 | 12 | #[derive(Default)] 13 | pub struct ModelBTreeMap 14 | where 15 | K: Eq + Ord, 16 | { 17 | data: Vec<(K, V)>, 18 | } 19 | 20 | impl ModelBTreeMap 21 | where 22 | K: Eq + Ord, 23 | { 24 | pub fn new() -> Self { 25 | Self { data: Vec::new() } 26 | } 27 | 28 | pub fn clear(&mut self) { 29 | self.data.clear(); 30 | } 31 | 32 | pub fn contains_key(&self, k: &K) -> bool { 33 | self.data.iter().any(|probe| probe.0 == *k) 34 | } 35 | 36 | pub fn get(&self, k: &K) -> Option<&V> { 37 | self.data.iter().find(|probe| probe.0 == *k).map(|e| &e.1) 38 | } 39 | 40 | pub fn get_key_value(&self, k: &K) -> Option<(&K, &V)> { 41 | self.data 42 | .iter() 43 | .find(|probe| probe.0 == *k) 44 | .map(|e| (&e.0, &e.1)) 45 | } 46 | 47 | pub fn get_mut(&mut self, k: &K) -> Option<&mut V> { 48 | self.data 49 | .iter_mut() 50 | .find(|probe| probe.0 == *k) 51 | .map(|e| &mut e.1) 52 | } 53 | 54 | pub fn insert(&mut self, k: K, v: V) -> Option { 55 | if let Some(e) = self.data.iter_mut().find(|probe| probe.0 == k) { 56 | Some(std::mem::replace(&mut e.1, v)) 57 | } else { 58 | self.data.push((k, v)); 59 | None 60 | } 61 | } 62 | 63 | pub fn is_empty(&self) -> bool { 64 | self.data.is_empty() 65 | } 66 | 67 | pub fn len(&self) -> usize { 68 | self.data.len() 69 | } 70 | 71 | pub fn remove(&mut self, k: &K) -> Option { 72 | let pos = self.data.iter().position(|probe| probe.0 == *k); 73 | pos.map(|idx| self.data.swap_remove(idx).1) 74 | } 75 | 76 | pub fn iter(&self) -> impl Iterator { 77 | self.data.iter().map(|e| (&e.0, &e.1)) 78 | } 79 | 80 | pub fn iter_mut(&mut self) -> impl Iterator { 81 | self.data.iter_mut().map(|e| (&e.0, &mut e.1)) 82 | } 83 | 84 | pub fn keys(&self) -> impl Iterator { 85 | self.data.iter().map(|e| &e.0) 86 | } 87 | 88 | pub fn range(&mut self, range: std::ops::Range) -> impl Iterator { 89 | self.range_mut(range).map(|e| (e.0, &*e.1)) 90 | } 91 | 92 | pub fn range_mut(&mut self, range: std::ops::Range) -> impl Iterator { 93 | self.data 94 | .iter_mut() 95 | .filter(move |e| e.0 >= range.start && e.0 < range.end) 96 | .map(|e| (&e.0, &mut e.1)) 97 | } 98 | 99 | pub fn split_off(&mut self, key: &K) -> impl IntoIterator { 100 | let (a, b) = self.data.drain(..).partition(|probe| probe.0 < *key); 101 | self.data = a; 102 | b 103 | } 104 | 105 | pub fn values(&self) -> impl Iterator { 106 | self.data.iter().map(|e| &e.1) 107 | } 108 | 109 | pub fn values_mut(&mut self) -> impl Iterator { 110 | self.data.iter_mut().map(|e| &mut e.1) 111 | } 112 | } 113 | 114 | fn sort_iterator>(i: I) -> Vec { 115 | let mut v: Vec<_> = i.collect::>(); 116 | v.sort(); 117 | v 118 | } 119 | 120 | fn sort_iterable>(i: I) -> Vec { 121 | let mut v: Vec<_> = i.into_iter().collect::>(); 122 | v.sort(); 123 | v 124 | } 125 | 126 | arbitrary_stateful_operations! { 127 | model = ModelBTreeMap, 128 | tested = BTreeMap, 129 | 130 | type_parameters = < 131 | K: Clone + Copy + Debug + Eq + Ord, 132 | V: Clone + Copy + Debug + Eq + Ord 133 | >, 134 | 135 | methods { 136 | equal { 137 | fn clear(&mut self); 138 | fn contains_key(&self, k: &K) -> bool; 139 | fn get(&self, k: &K) -> Option<&V>; 140 | fn get_key_value(&self, k: &K) -> Option<(&K, &V)>; 141 | fn get_mut(&mut self, k: &K) -> Option<&mut V>; 142 | fn insert(&mut self, k: K, v: V) -> Option; 143 | fn is_empty(&self) -> bool; 144 | fn len(&self) -> usize; 145 | fn remove(&mut self, k: &K) -> Option; 146 | } 147 | 148 | equal_with(sort_iterator) { 149 | fn iter(&self) -> impl Iterator; 150 | fn iter_mut(&self) -> impl Iterator; 151 | fn keys(&self) -> impl Iterator; 152 | fn range(&self, range: std::ops::Range) -> impl Iterator; 153 | fn range_mut(&self, range: std::ops::Range) -> impl Iterator; 154 | fn values(&self) -> impl Iterator; 155 | fn values_mut(&mut self) -> impl Iterator; 156 | } 157 | 158 | equal_with(sort_iterable) { 159 | fn split_off(&mut self, k: &K) -> impl IntoIterator; 160 | } 161 | } 162 | } 163 | 164 | #[allow(clippy::unnecessary_wraps)] 165 | fn fuzz_cycle(data: &[u8]) -> arbitrary::Result<()> { 166 | use arbitrary::{Arbitrary, Unstructured}; 167 | 168 | let mut ring = Unstructured::new(data); 169 | let mut model = ModelBTreeMap::::new(); 170 | let mut tested = BTreeMap::::new(); 171 | 172 | let mut op_trace = String::new(); 173 | while let Ok(op) = as Arbitrary>::arbitrary(&mut ring) { 174 | op.append_to_trace(&mut op_trace); 175 | op.execute_and_compare(&mut model, &mut tested); 176 | } 177 | 178 | Ok(()) 179 | } 180 | 181 | fn main() -> Result<(), ()> { 182 | better_panic::install(); 183 | 184 | loop { 185 | fuzz!(|data: &[u8]| { 186 | let _ = fuzz_cycle(data); 187 | }); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/src/index_map.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::manual_find_map)] 2 | #![allow(clippy::must_use_candidate)] 3 | #![allow(clippy::option_if_let_else)] 4 | 5 | use honggfuzz::fuzz; 6 | use rutenspitz::arbitrary_stateful_operations; 7 | 8 | use indexmap::IndexMap; 9 | 10 | use std::fmt::Debug; 11 | use std::hash::Hash; 12 | 13 | #[derive(Default)] 14 | pub struct ModelHashMap 15 | where 16 | K: Eq + Hash, 17 | { 18 | data: Vec<(K, V)>, 19 | } 20 | 21 | impl ModelHashMap 22 | where 23 | K: Eq + Hash, 24 | { 25 | pub fn clear(&mut self) { 26 | self.data.clear(); 27 | } 28 | 29 | pub fn contains_key(&self, k: &K) -> bool { 30 | self.data.iter().any(|probe| probe.0 == *k) 31 | } 32 | 33 | pub fn get(&self, k: &K) -> Option<&V> { 34 | self.data.iter().find(|probe| probe.0 == *k).map(|e| &e.1) 35 | } 36 | 37 | pub fn get_full(&self, k: &K) -> Option<(usize, &K, &V)> { 38 | self.data 39 | .iter() 40 | .enumerate() 41 | .find(|(_, probe)| probe.0 == *k) 42 | .map(|(i, e)| (i, &e.0, &e.1)) 43 | } 44 | 45 | pub fn get_full_mut(&mut self, k: &K) -> Option<(usize, &K, &mut V)> { 46 | self.data 47 | .iter_mut() 48 | .enumerate() 49 | .find(|(_, probe)| probe.0 == *k) 50 | .map(|(i, e)| (i, &e.0, &mut e.1)) 51 | } 52 | 53 | pub fn get_index(&self, index: usize) -> Option<(&K, &V)> { 54 | self.data.get(index).map(|el| (&el.0, &el.1)) 55 | } 56 | 57 | pub fn get_index_mut(&mut self, index: usize) -> Option<(&mut K, &mut V)> { 58 | self.data.get_mut(index).map(|el| (&mut el.0, &mut el.1)) 59 | } 60 | 61 | pub fn get_mut(&mut self, k: &K) -> Option<&mut V> { 62 | self.data 63 | .iter_mut() 64 | .find(|probe| probe.0 == *k) 65 | .map(|e| &mut e.1) 66 | } 67 | 68 | pub fn insert(&mut self, k: K, v: V) -> Option { 69 | if let Some(e) = self.data.iter_mut().find(|probe| probe.0 == k) { 70 | Some(std::mem::replace(&mut e.1, v)) 71 | } else { 72 | self.data.push((k, v)); 73 | None 74 | } 75 | } 76 | 77 | pub fn is_empty(&self) -> bool { 78 | self.data.is_empty() 79 | } 80 | 81 | pub fn len(&self) -> usize { 82 | self.data.len() 83 | } 84 | 85 | pub fn pop(&mut self) -> Option<(K, V)> { 86 | self.data.pop() 87 | } 88 | 89 | pub fn swap_remove(&mut self, key: &K) -> Option { 90 | self.swap_remove_full(key).map(|(_, _, v)| v) 91 | } 92 | 93 | pub fn swap_remove_full(&mut self, key: &K) -> Option<(usize, K, V)> { 94 | let pos = self.data.iter().position(|probe| probe.0 == *key); 95 | pos.map(|idx| (idx, self.data.swap_remove(idx))) 96 | .map(|(idx, (k, v))| (idx, k, v)) 97 | } 98 | 99 | pub fn swap_remove_index(&mut self, index: usize) -> Option<(K, V)> { 100 | if index >= self.data.len() { 101 | return None; 102 | } 103 | Some(self.data.swap_remove(index)) 104 | } 105 | 106 | pub fn iter(&self) -> impl Iterator { 107 | self.data.iter().map(|e| (&e.0, &e.1)) 108 | } 109 | 110 | pub fn iter_mut(&mut self) -> impl Iterator { 111 | self.data.iter_mut().map(|e| (&e.0, &mut e.1)) 112 | } 113 | 114 | pub fn keys(&self) -> impl Iterator { 115 | self.data.iter().map(|e| &e.0) 116 | } 117 | 118 | pub fn values(&self) -> impl Iterator { 119 | self.data.iter().map(|e| &e.1) 120 | } 121 | 122 | pub fn values_mut(&mut self) -> impl Iterator { 123 | self.data.iter_mut().map(|e| &mut e.1) 124 | } 125 | } 126 | 127 | fn sort_iterator>(i: I) -> Vec { 128 | let mut v: Vec<_> = i.collect::>(); 129 | v.sort(); 130 | v 131 | } 132 | 133 | arbitrary_stateful_operations! { 134 | model = ModelHashMap, 135 | tested = IndexMap, 136 | 137 | type_parameters = < 138 | K: Clone + Copy + Debug + Eq + Hash + Ord, 139 | V: Clone + Debug + Eq + Ord 140 | >, 141 | 142 | methods { 143 | equal { 144 | fn clear(&mut self); 145 | fn contains_key(&self, k: &K) -> bool; 146 | fn get(&self, k: &K) -> Option<&V>; 147 | fn get_full(&self, k: &K) -> Option<(usize, &K, &V)>; 148 | fn get_full_mut(&mut self, k: &K) -> Option<(usize, &K, &mut V)>; 149 | fn get_index(&self, index: usize) -> Option<(&K, &V)>; 150 | fn get_index_mut(&mut self, index: usize) -> Option<(&mut K, &mut V)>; 151 | fn get_mut(&mut self, k: &K) -> Option<&mut V>; 152 | fn insert(&mut self, k: K, v: V) -> Option; 153 | fn is_empty(&self) -> bool; 154 | fn len(&self) -> usize; 155 | fn pop(&mut self) -> Option<(K, V)>; 156 | fn swap_remove(&mut self, key: &K) -> Option; 157 | fn swap_remove_full(&mut self, key: &K) -> Option<(usize, K, V)>; 158 | fn swap_remove_index(&mut self, index: usize) -> Option<(K, V)>; 159 | } 160 | 161 | equal_with(sort_iterator) { 162 | fn iter(&self) -> impl Iterator; 163 | fn iter_mut(&self) -> impl Iterator; 164 | fn keys(&self) -> impl Iterator; 165 | fn values(&self) -> impl Iterator; 166 | fn values_mut(&mut self) -> impl Iterator; 167 | } 168 | } 169 | } 170 | 171 | fn fuzz_cycle(data: &[u8]) -> arbitrary::Result<()> { 172 | use arbitrary::{Arbitrary, Unstructured}; 173 | 174 | let mut ring = Unstructured::new(data); 175 | let capacity: u8 = Arbitrary::arbitrary(&mut ring)?; 176 | 177 | let mut model = ModelHashMap::::default(); 178 | let mut tested = IndexMap::::with_capacity(capacity as usize); 179 | 180 | let mut op_trace = String::new(); 181 | while let Ok(op) = as Arbitrary>::arbitrary(&mut ring) { 182 | op.append_to_trace(&mut op_trace); 183 | op.execute_and_compare(&mut model, &mut tested); 184 | } 185 | 186 | Ok(()) 187 | } 188 | 189 | fn main() -> Result<(), ()> { 190 | better_panic::install(); 191 | 192 | loop { 193 | fuzz!(|data: &[u8]| { 194 | let _ = fuzz_cycle(data); 195 | }); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /examples/src/hash_map.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cast_possible_truncation)] 2 | #![allow(clippy::manual_find_map)] 3 | #![allow(clippy::must_use_candidate)] 4 | #![allow(clippy::option_if_let_else)] 5 | 6 | use honggfuzz::fuzz; 7 | use rutenspitz::arbitrary_stateful_operations; 8 | 9 | use hashbrown::HashMap; 10 | 11 | use std::fmt::Debug; 12 | use std::hash::{BuildHasher, Hash}; 13 | 14 | pub struct BuildAHasher { 15 | seed: u128, 16 | } 17 | 18 | impl BuildAHasher { 19 | pub fn new(seed: u128) -> Self { 20 | Self { seed } 21 | } 22 | } 23 | 24 | impl BuildHasher for BuildAHasher { 25 | type Hasher = ahash::AHasher; 26 | 27 | fn build_hasher(&self) -> Self::Hasher { 28 | ahash::RandomState::with_seed(self.seed as usize).build_hasher() 29 | } 30 | } 31 | 32 | #[derive(Default)] 33 | pub struct ModelHashMap 34 | where 35 | K: Eq + Hash, 36 | { 37 | data: Vec<(K, V)>, 38 | } 39 | 40 | impl ModelHashMap 41 | where 42 | K: Eq + Hash, 43 | { 44 | pub fn clear(&mut self) { 45 | self.data.clear(); 46 | } 47 | 48 | pub fn contains_key(&self, k: &K) -> bool { 49 | self.data.iter().any(|probe| probe.0 == *k) 50 | } 51 | 52 | pub fn get(&self, k: &K) -> Option<&V> { 53 | self.data.iter().find(|probe| probe.0 == *k).map(|e| &e.1) 54 | } 55 | 56 | pub fn get_key_value(&self, k: &K) -> Option<(&K, &V)> { 57 | self.data 58 | .iter() 59 | .find(|probe| probe.0 == *k) 60 | .map(|e| (&e.0, &e.1)) 61 | } 62 | 63 | pub fn get_mut(&mut self, k: &K) -> Option<&mut V> { 64 | self.data 65 | .iter_mut() 66 | .find(|probe| probe.0 == *k) 67 | .map(|e| &mut e.1) 68 | } 69 | 70 | pub fn insert(&mut self, k: K, v: V) -> Option { 71 | if let Some(e) = self.data.iter_mut().find(|probe| probe.0 == k) { 72 | Some(std::mem::replace(&mut e.1, v)) 73 | } else { 74 | self.data.push((k, v)); 75 | None 76 | } 77 | } 78 | 79 | pub fn is_empty(&self) -> bool { 80 | self.data.is_empty() 81 | } 82 | 83 | pub fn len(&self) -> usize { 84 | self.data.len() 85 | } 86 | 87 | pub fn remove(&mut self, k: &K) -> Option { 88 | let pos = self.data.iter().position(|probe| probe.0 == *k); 89 | pos.map(|idx| self.data.swap_remove(idx).1) 90 | } 91 | 92 | pub fn shrink_to(&mut self, min_capacity: usize) { 93 | self.data.shrink_to(std::cmp::min( 94 | self.data.capacity(), 95 | std::cmp::max(min_capacity, self.data.len()), 96 | )); 97 | } 98 | 99 | pub fn shrink_to_fit(&mut self) { 100 | self.data.shrink_to_fit(); 101 | } 102 | 103 | pub fn drain(&mut self) -> impl Iterator + '_ { 104 | self.data.drain(..) 105 | } 106 | 107 | pub fn iter(&self) -> impl Iterator { 108 | self.data.iter().map(|e| (&e.0, &e.1)) 109 | } 110 | 111 | pub fn iter_mut(&mut self) -> impl Iterator { 112 | self.data.iter_mut().map(|e| (&e.0, &mut e.1)) 113 | } 114 | 115 | pub fn keys(&self) -> impl Iterator { 116 | self.data.iter().map(|e| &e.0) 117 | } 118 | 119 | pub fn values(&self) -> impl Iterator { 120 | self.data.iter().map(|e| &e.1) 121 | } 122 | 123 | pub fn values_mut(&mut self) -> impl Iterator { 124 | self.data.iter_mut().map(|e| &mut e.1) 125 | } 126 | } 127 | 128 | fn sort_iterator>(i: I) -> Vec { 129 | let mut v: Vec<_> = i.collect::>(); 130 | v.sort(); 131 | v 132 | } 133 | 134 | arbitrary_stateful_operations! { 135 | model = ModelHashMap, 136 | tested = HashMap, 137 | 138 | type_parameters = < 139 | K: Clone + Debug + Eq + Hash + Ord, 140 | V: Clone + Debug + Eq + Ord 141 | >, 142 | 143 | methods { 144 | equal { 145 | fn clear(&mut self); 146 | fn contains_key(&self, k: &K) -> bool; 147 | fn get(&self, k: &K) -> Option<&V>; 148 | fn get_key_value(&self, k: &K) -> Option<(&K, &V)>; 149 | fn get_mut(&mut self, k: &K) -> Option<&mut V>; 150 | fn insert(&mut self, k: K, v: V) -> Option; 151 | fn remove(&mut self, k: &K) -> Option; 152 | fn shrink_to(&mut self, min_capacity: usize); 153 | fn shrink_to_fit(&mut self); 154 | } 155 | 156 | equal_with(sort_iterator) { 157 | fn drain(&mut self) -> impl Iterator; 158 | fn iter(&self) -> impl Iterator; 159 | fn iter_mut(&self) -> impl Iterator; 160 | fn keys(&self) -> impl Iterator; 161 | fn values(&self) -> impl Iterator; 162 | fn values_mut(&mut self) -> impl Iterator; 163 | } 164 | } 165 | 166 | pre { 167 | let prev_capacity = tested.capacity(); 168 | } 169 | 170 | post { 171 | if op_name == "clear" { 172 | assert_eq!(tested.capacity(), prev_capacity, 173 | "capacity: {}, previous: {}", 174 | tested.capacity(), prev_capacity); 175 | } 176 | 177 | assert!(tested.capacity() >= model.len()); 178 | assert_eq!(tested.is_empty(), model.is_empty()); 179 | assert_eq!(tested.len(), model.len()); 180 | } 181 | } 182 | 183 | fn fuzz_cycle(data: &[u8]) -> arbitrary::Result<()> { 184 | use arbitrary::{Arbitrary, Unstructured}; 185 | 186 | let mut ring = Unstructured::new(data); 187 | 188 | let capacity: u16 = Arbitrary::arbitrary(&mut ring)?; 189 | let hash_seed: u128 = Arbitrary::arbitrary(&mut ring)?; 190 | 191 | let mut model = ModelHashMap::::default(); 192 | let mut tested: HashMap = 193 | HashMap::with_capacity_and_hasher(capacity as usize, BuildAHasher::new(hash_seed)); 194 | 195 | let mut op_trace = String::new(); 196 | while let Ok(op) = as Arbitrary>::arbitrary(&mut ring) { 197 | op.append_to_trace(&mut op_trace); 198 | op.execute_and_compare(&mut model, &mut tested); 199 | } 200 | 201 | Ok(()) 202 | } 203 | 204 | fn main() -> Result<(), ()> { 205 | better_panic::install(); 206 | 207 | loop { 208 | fuzz!(|data: &[u8]| { 209 | let _ = fuzz_cycle(data); 210 | }); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /DEBUGGING.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | The `Op` struct implements the `Display` trait that produces semi-valid (usually valid) Rust code equivalent to the operation (e.g. `v.insert(0, 2);` for an insert operation of a HashMap). This can be very usefully employed when debugging an actual crash found by the fuzzer, such that from the debugger we have access to an almost compile-ready operation trace that led to the given crash. See [these two lines](examples/src/hash_map.rs#L197-L198) for an example of how this trace is built up during the fuzzing process: 4 | 5 | https://github.com/jakubadamw/rutenspitz/blob/40cf9660493e3e96e63a7d3da296665d59f8bef6/examples/src/hash_map.rs#L197-L198 6 | 7 | Such a trace can later be used as a starting point for an investigation into the root cause of a bug found by the fuzzing. 8 | 9 | Say, we run the `hash_map` fuzz test: 10 | 11 | ```sh 12 | cargo hfuzz run hash_map 13 | ``` 14 | 15 | And it finds a crash. 16 | 17 | ![fuzzing result](./img/honggfuzz.png) 18 | 19 | We run it again, but this time through the debugger. 20 | 21 | ```sh 22 | cargo hfuzz run-debug hash_map hfuzz_workspace/hash_map/*fuzz 23 | ``` 24 | 25 | As is known, the fuzzing tests must be deterministic with regards to the bytes they're fed with. In the case of the hash map test, the random seed it uses for the hasher initialisation is also derived from the input bytes. Because of it, it is indeed deterministic and we're able to reproduce the crash when running it again (with the saved snapshot of the fuzzing data as an input) from the debugger. 26 | 27 | ```gdb 28 | HFUZZ_DEBUGGER=lldb cargo hfuzz run-debug hash_map hfuzz_workspace/hash_map/*fuzz 29 | /usr/bin/ld.gold 30 | Finished dev [unoptimized + debuginfo] target(s) in 0.09s 31 | (lldb) target create "hfuzz_target/x86_64-unknown-linux-gnu/debug/hash_map" 32 | Current executable set to '/home/jakubw/rutenspitz/hfuzz_target/x86_64-unknown-linux-gnu/debug/hash_map' (x86_64). 33 | (lldb) b rust_panic 34 | Breakpoint 1: where = hash_map`rust_panic + 19 at panicking.rs:575:9, address = 0x00000000001689a3 35 | (lldb) r 36 | Backtrace (most recent call first): 37 | File "/home/jakubw/rutenspitz/examples/src/hash_map.rs", line 180, in hash_map::op::Op::execute_and_compare 38 | assert_eq!(tested.len(), model.len()); 39 | File "/home/jakubw/rutenspitz/examples/src/hash_map.rs", line 200, in hash_map::fuzz_cycle 40 | op.execute_and_compare(&mut model, &mut tested); 41 | File "/home/jakubw/rutenspitz/examples/src/hash_map.rs", line 211, in hash_map::main::{{closure}} 42 | let _ = fuzz_cycle(data); 43 | File "/home/jakubw/.cargo/registry/src/github.com-1ecc6299db9ec823/honggfuzz-0.5.49/src/lib.rs", line 329, in honggfuzz::fuzz 44 | closure(&mmap); 45 | File "/home/jakubw/rutenspitz/examples/src/hash_map.rs", line 210, in hash_map::main 46 | fuzz!(|data: &[u8]| { 47 | 48 | The application panicked (crashed). 49 | assertion failed: `(left == right)` 50 | left: `11`, 51 | right: `10` 52 | in examples/src/hash_map.rs, line 180 53 | thread: main 54 | Process 3272921 stopped 55 | * thread #1, name = 'hash_map', stop reason = breakpoint 1.1 56 | frame #0: 0x00005555556bc9a3 hash_map`rust_panic at panicking.rs:575:9 57 | 58 | Process 3274281 launched: '/home/jakubw/rutenspitz/hfuzz_target/x86_64-unknown-linux-gnu/debug/hash_map' (x86_64) 59 | (lldb) bt 60 | * thread #1, name = 'hash_map', stop reason = breakpoint 1.1 61 | * frame #0: 0x00005555556bc9a3 hash_map`rust_panic at panicking.rs:575:9 62 | frame #1: 0x00005555556bc95a hash_map`std::panicking::rust_panic_with_hook::hb7ad549fb7110aed at panicking.rs:545:5 63 | frame #2: 0x00005555556bc4cb hash_map`rust_begin_unwind at panicking.rs:437:5 64 | frame #3: 0x00005555556bc43b hash_map`std::panicking::begin_panic_fmt::hb90a7d6c31a2e780 at panicking.rs:391:5 65 | frame #4: 0x00005555555a2cde hash_map`hash_map::op::Op$LT$K$C$V$GT$::execute_and_compare::h76253904ace04422(self=Op @ 0x00007fffffffdc20, model=0x00007fffffffdb08, tested=0x00007 66 | fffffffdb20) at hash_map.rs:180:9 67 | frame #5: 0x000055555559d3a3 hash_map`hash_map::fuzz_cycle::h1b91651e06656ad8(data=(data_ptr = "\x12\x8ek, length = 3757)) at hash_map.rs:200:9 68 | frame #6: 0x000055555558ddf3 hash_map`hash_map::main::_$u7b$$u7b$closure$u7d$$u7d$::h8a51d7c412d8f5fe((null)=closure-0 @ 0x00007fffffffdc80, data=(data_ptr = "\x12\x8ek, length = 3757)) at 69 | hash_map.rs:211:21 70 | frame #7: 0x00005555555769df hash_map`honggfuzz::fuzz::he52927812c14de56(closure=closure-0 @ 0x00007fffffffddf0) at lib.rs:329:5 71 | frame #8: 0x000055555559d45e hash_map`hash_map::main::h6d8216e6d238caab at hash_map.rs:210:9 72 | frame #9: 0x00005555555a9e2b hash_map`std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h5f0897ab382e4c10 at rt.rs:67:34 73 | frame #10: 0x00005555556bcca3 hash_map`std::rt::lang_start_internal::heeef42c9aa2e7f9b [inlined] std::rt::lang_start_internal::_$u7b$$u7b$closure$u7d$$u7d$::hac40205b257c5248 at rt.rs:52:1 74 | 3 75 | frame #11: 0x00005555556bcc98 hash_map`std::rt::lang_start_internal::heeef42c9aa2e7f9b [inlined] std::panicking::try::do_call::h28f2e69fa50926c2 at panicking.rs:348 76 | frame #12: 0x00005555556bcc98 hash_map`std::rt::lang_start_internal::heeef42c9aa2e7f9b [inlined] std::panicking::try::hb2c13e7a9a68aa8b at panicking.rs:325 77 | frame #13: 0x00005555556bcc98 hash_map`std::rt::lang_start_internal::heeef42c9aa2e7f9b [inlined] std::panic::catch_unwind::hb5c1b6ef4106c1a1 at panic.rs:394 78 | frame #14: 0x00005555556bcc98 hash_map`std::rt::lang_start_internal::heeef42c9aa2e7f9b at rt.rs:51 79 | frame #15: 0x00005555555a9e07 hash_map`std::rt::lang_start::h3f011cb1aa45d1f4(main=(hash_map`hash_map::main::h6d8216e6d238caab at hash_map.rs:206), argc=1, argv=0x00007fffffffe0d8) at rt.r 80 | s:67:5 81 | frame #16: 0x000055555559d48a hash_map`main + 42 82 | frame #17: 0x00007ffff7df20b3 libc.so.6`__libc_start_main + 243 83 | frame #18: 0x00005555555760ae hash_map`_start + 46 84 | ``` 85 | 86 | We now look for the top-most frame that's located in the `fuzz_cycle` function. It's frame number 5. 87 | 88 | ```gdb 89 | frame #5: 0x000055555559d3a3 hash_map`hash_map::fuzz_cycle::h1b91651e06656ad8(data=(data_ptr = "\x12\x8ek, length = 3757)) at hash_map.rs:200:9 90 | ``` 91 | 92 | We select it. 93 | 94 | ```gdb 95 | (lldb) frame select 5 96 | frame #5: 0x000055555559d3a3 hash_map`hash_map::fuzz_cycle::h1b91651e06656ad8(data=(data_ptr = "\x12\x8ek, length = 3757)) at hash_map.rs:200:9 97 | 197 while let Ok(op) = as Arbitrary>::arbitrary(&mut ring) { 98 | 198 #[cfg(fuzzing_debug)] 99 | 199 _op_trace.push_str(&format!("{}\n", op.to_string())); 100 | -> 200 op.execute_and_compare(&mut model, &mut tested); 101 | 201 } 102 | 202 103 | 203 Ok(()) 104 | ``` 105 | 106 | And we're able to print out the op trace: 107 | 108 | ``` 109 | (lldb) expr (void) puts(_op_trace.vec.buf.ptr.pointer) 110 | (…) 111 | v.get_key_value(&21624); 112 | v.iter(); 113 | v.values(); 114 | v.shrink_to_fit(); 115 | v.drain(); 116 | v.values_mut(); 117 | v.shrink_to_fit(); 118 | v.values(); 119 | v.insert(14415, 18726); 120 | v.insert(30298, 60880); 121 | v.iter_mut(); 122 | v.values_mut(); 123 | v.get_mut(&5287); 124 | v.contains_key(&1056); 125 | v.values(); 126 | v.values_mut(); 127 | v.insert(42387, 40681); 128 | v.insert(64012, 21197); 129 | v.values_mut(); 130 | v.values(); 131 | v.insert(20656, 60976); 132 | v.contains_key(&48925); 133 | v.values(); 134 | v.remove(&45196); 135 | v.insert(52201, 27410); 136 | ``` 137 | 138 | Which we can copy out or write out to a file (using, for example, https://github.com/4iar/lldb-write). 139 | -------------------------------------------------------------------------------- /proc_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use either::Either; 2 | use proc_macro as pm; 3 | use proc_macro2 as pm2; 4 | use quote::quote; 5 | use syn::parse_macro_input; 6 | use syn::spanned::Spanned; 7 | 8 | mod kw { 9 | syn::custom_keyword!(equal); 10 | syn::custom_keyword!(equal_with); 11 | syn::custom_keyword!(methods); 12 | syn::custom_keyword!(model); 13 | syn::custom_keyword!(post); 14 | syn::custom_keyword!(pre); 15 | syn::custom_keyword!(tested); 16 | syn::custom_keyword!(type_parameters); 17 | } 18 | 19 | #[allow(clippy::enum_variant_names)] 20 | enum PassingMode { 21 | ByValue, 22 | ByRef, 23 | ByRefMut, 24 | } 25 | 26 | struct Argument { 27 | name: syn::Ident, 28 | ty: syn::Type, 29 | passing_mode: PassingMode, 30 | } 31 | 32 | struct Method { 33 | name: syn::Ident, 34 | // self_mut: bool, 35 | inputs: Vec, 36 | process_result: Option, 37 | // output: syn::Type 38 | } 39 | 40 | impl syn::parse::Parse for Method { 41 | fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { 42 | let method_item: syn::TraitItemFn = input.parse()?; 43 | 44 | if let Some(ref defaultness) = method_item.default { 45 | return Err(syn::Error::new(defaultness.span(), "unexpected `default`")); 46 | } 47 | if let Some(ref constness) = method_item.sig.constness { 48 | return Err(syn::Error::new(constness.span(), "unexpected `const`")); 49 | } 50 | if let Some(ref asyncness) = method_item.sig.asyncness { 51 | return Err(syn::Error::new(asyncness.span(), "unexpected `async`")); 52 | } 53 | if let Some(ref unsafety) = method_item.sig.unsafety { 54 | return Err(syn::Error::new(unsafety.span(), "unexpected `unsafe`")); 55 | } 56 | 57 | let (receivers, args) = method_item 58 | .sig 59 | .inputs 60 | .iter() 61 | .map(|input| match input { 62 | syn::FnArg::Receiver(receiver) => Either::Left(receiver), 63 | syn::FnArg::Typed(syn::PatType { ty, pat, .. }) => { 64 | let ident = match **pat { 65 | syn::Pat::Ident(syn::PatIdent { ref ident, .. }) => ident.clone(), 66 | ref pat => syn::Ident::new("_", pat.span()), 67 | }; 68 | match **ty { 69 | syn::Type::Reference(syn::TypeReference { 70 | ref mutability, 71 | ref elem, 72 | .. 73 | }) => Either::Right(Argument { 74 | name: ident, 75 | ty: (**elem).clone(), 76 | passing_mode: if mutability.is_some() { 77 | PassingMode::ByRefMut 78 | } else { 79 | PassingMode::ByRef 80 | }, 81 | }), 82 | ref ty => Either::Right(Argument { 83 | name: ident, 84 | ty: ty.clone(), 85 | passing_mode: PassingMode::ByValue, 86 | }), 87 | } 88 | } 89 | }) 90 | .partition::, _>(Either::is_left); 91 | 92 | let receivers: Vec<_> = receivers.into_iter().filter_map(Either::left).collect(); 93 | let args: Vec<_> = args.into_iter().filter_map(Either::right).collect(); 94 | 95 | let receiver = receivers.first(); 96 | if let Some(receiver) = receiver { 97 | if receiver.reference.is_none() { 98 | return Err(syn::Error::new( 99 | receiver.span(), 100 | "unexpected by-value receiver", 101 | )); 102 | } 103 | } else { 104 | return Err(syn::Error::new( 105 | method_item.span(), 106 | "unexpected method with no receiver", 107 | )); 108 | } 109 | 110 | Ok(Self { 111 | name: method_item.sig.ident, 112 | // self_mut: receiver.map_or(false, |r| r.mutability.is_some()), 113 | process_result: None, 114 | inputs: args, 115 | /*output: match method_item.sig.output { 116 | syn::ReturnType::Default => 117 | syn::parse_str("()").unwrap(), 118 | syn::ReturnType::Type(_, typ) => 119 | (*typ).clone() 120 | }*/ 121 | }) 122 | } 123 | } 124 | 125 | struct Specification { 126 | model: syn::Path, 127 | tested: syn::Path, 128 | lifetimes: Vec, 129 | type_params: Vec, 130 | methods: Vec, 131 | post: Vec, 132 | pre: Vec, 133 | } 134 | 135 | impl syn::parse::Parse for Specification { 136 | fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { 137 | use syn::{braced, parenthesized, Token}; 138 | 139 | let mut model: Option = None; 140 | let mut tested: Option = None; 141 | let mut lifetimes: Vec = vec![]; 142 | let mut type_params: Vec = vec![]; 143 | let mut methods: Vec = vec![]; 144 | let mut post: Vec = vec![]; 145 | let mut pre: Vec = vec![]; 146 | 147 | while !input.is_empty() { 148 | let lookahead = input.lookahead1(); 149 | if lookahead.peek(kw::model) { 150 | let _: kw::model = input.parse()?; 151 | let _: Token![=] = input.parse()?; 152 | model = Some(input.parse()?); 153 | } else if lookahead.peek(kw::tested) { 154 | let _: kw::tested = input.parse()?; 155 | let _: Token![=] = input.parse()?; 156 | tested = Some(input.parse()?); 157 | } else if lookahead.peek(kw::type_parameters) { 158 | let _: kw::type_parameters = input.parse()?; 159 | let _: Token![=] = input.parse()?; 160 | let generics: syn::Generics = input.parse()?; 161 | lifetimes = generics.lifetimes().cloned().collect(); 162 | type_params = generics.type_params().cloned().collect(); 163 | } else if lookahead.peek(kw::methods) { 164 | let outer; 165 | let mut inner; 166 | let _: kw::methods = input.parse()?; 167 | braced!(outer in input); 168 | 169 | while !outer.is_empty() { 170 | let lookahead = outer.lookahead1(); 171 | let process = if lookahead.peek(kw::equal) { 172 | let _: kw::equal = outer.parse()?; 173 | None 174 | } else if lookahead.peek(kw::equal_with) { 175 | let _: kw::equal_with = outer.parse()?; 176 | let path; 177 | parenthesized!(path in outer); 178 | Some(path.parse()?) 179 | } else { 180 | return Err(lookahead.error()); 181 | }; 182 | 183 | braced!(inner in outer); 184 | while !inner.is_empty() { 185 | let mut method: Method = inner.parse()?; 186 | method.process_result = process.clone(); 187 | methods.push(method); 188 | } 189 | } 190 | } else if lookahead.peek(kw::post) { 191 | let inner; 192 | let _: kw::post = input.parse()?; 193 | braced!(inner in input); 194 | while !inner.is_empty() { 195 | post.push(inner.parse()?); 196 | } 197 | } else if lookahead.peek(kw::pre) { 198 | let inner; 199 | let _: kw::pre = input.parse()?; 200 | braced!(inner in input); 201 | while !inner.is_empty() { 202 | pre.push(inner.parse()?); 203 | } 204 | } else { 205 | return Err(lookahead.error()); 206 | } 207 | 208 | if input.peek(Token![,]) { 209 | let _: Token![,] = input.parse()?; 210 | } 211 | } 212 | 213 | let model = model.ok_or_else(|| input.error("missing `model`"))?; 214 | let tested = tested.ok_or_else(|| input.error("missing `tested`"))?; 215 | 216 | Ok(Self { 217 | model, 218 | tested, 219 | lifetimes, 220 | type_params, 221 | methods, 222 | post, 223 | pre, 224 | }) 225 | } 226 | } 227 | 228 | impl quote::ToTokens for Method { 229 | fn to_tokens(&self, tokens: &mut pm2::TokenStream) { 230 | use pm2::{Delimiter, Group, Ident, Punct, Spacing, Span}; 231 | use quote::TokenStreamExt; 232 | 233 | tokens.append(self.name.clone()); 234 | 235 | if !self.inputs.is_empty() { 236 | let mut fields = pm2::TokenStream::new(); 237 | for input in &self.inputs { 238 | fields.append(input.name.clone()); 239 | fields.append(Punct::new(':', Spacing::Joint)); 240 | if let syn::Type::Slice(_) = input.ty { 241 | fields.append(Ident::new("Box", Span::call_site())); 242 | fields.append(Punct::new('<', Spacing::Joint)); 243 | input.ty.to_tokens(&mut fields); 244 | fields.append(Punct::new('>', Spacing::Joint)); 245 | } else { 246 | input.ty.to_tokens(&mut fields); 247 | } 248 | fields.append(Punct::new(',', Spacing::Joint)); 249 | } 250 | tokens.append(Group::new(Delimiter::Brace, fields)); 251 | } 252 | } 253 | } 254 | 255 | struct MethodTest<'s> { 256 | method: &'s Method, 257 | compare: bool, 258 | } 259 | 260 | impl<'s> quote::ToTokens for MethodTest<'s> { 261 | #[allow(clippy::too_many_lines)] 262 | fn to_tokens(&self, tokens: &mut pm2::TokenStream) { 263 | let args: Vec<_> = self 264 | .method 265 | .inputs 266 | .iter() 267 | .map(|input| { 268 | let input_name = &input.name; 269 | match input.passing_mode { 270 | PassingMode::ByValue => quote! { #input_name.clone() }, 271 | PassingMode::ByRef => quote! { #input_name }, 272 | PassingMode::ByRefMut => quote! { &mut *#input_name }, 273 | } 274 | }) 275 | .collect(); 276 | 277 | let method_name = &self.method.name; 278 | 279 | let keys: Vec<_> = self.method.inputs.iter().map(|input| &input.name).collect(); 280 | let pattern = if keys.is_empty() { 281 | quote! { Op::#method_name } 282 | } else { 283 | quote! { Op::#method_name { #(ref #keys),* } } 284 | }; 285 | 286 | let process_tested_ret_value = self 287 | .method 288 | .process_result 289 | .as_ref() 290 | .map(|p| quote! { #p(tested_ret_value) }) 291 | .unwrap_or(quote! { tested_ret_value }); 292 | 293 | if self.compare { 294 | let process_model_ret_value = self 295 | .method 296 | .process_result 297 | .as_ref() 298 | .map(|p| quote! { #p(model_ret_value) }) 299 | .unwrap_or(quote! { model_ret_value }); 300 | tokens.extend(quote! { 301 | #pattern => { 302 | enum Outcome { 303 | Equal, 304 | #[cfg(not(fuzzing_debug))] 305 | Unequal, 306 | #[cfg(fuzzing_debug)] 307 | Unequal { 308 | model_ret_value_debug: String, 309 | tested_ret_value_debug: String, 310 | }, 311 | } 312 | 313 | enum WhichFailed { 314 | None(Outcome), 315 | First, 316 | Second, 317 | } 318 | 319 | struct GalaxyBrain<'a> { 320 | value: WhichFailed, 321 | to_update: &'a mut WhichFailed, 322 | } 323 | 324 | impl<'a> Drop for GalaxyBrain<'a> { 325 | fn drop(&mut self) { 326 | std::mem::swap(self.to_update, &mut self.value); 327 | } 328 | } 329 | 330 | let mut f = WhichFailed::First; 331 | 332 | { 333 | let mut guard = GalaxyBrain { 334 | value: WhichFailed::First, 335 | to_update: &mut f, 336 | }; 337 | 338 | let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 339 | let model_ret_value = model.#method_name(#(#args),*); 340 | guard.value = WhichFailed::Second; 341 | let tested_ret_value = tested.#method_name(#(#args),*); 342 | 343 | let model_ret_value = #process_model_ret_value; 344 | let tested_ret_value = #process_tested_ret_value; 345 | 346 | let outcome = if model_ret_value == tested_ret_value { 347 | Outcome::Equal 348 | } else { 349 | #[cfg(fuzzing_debug)] 350 | { 351 | Outcome::Unequal { 352 | model_ret_value_debug: format!("{:?}", model_ret_value), 353 | tested_ret_value_debug: format!("{:?}", tested_ret_value), 354 | } 355 | } 356 | #[cfg(not(fuzzing_debug))] 357 | Outcome::Unequal 358 | }; 359 | guard.value = WhichFailed::None(outcome); 360 | })); 361 | } 362 | 363 | match f { 364 | WhichFailed::None(outcome) => { 365 | #[cfg(fuzzing_debug)] 366 | if let Outcome::Unequal { model_ret_value_debug, tested_ret_value_debug } = outcome { 367 | rutenspitz::panic!( 368 | "The return values aren't equal: `{}` != `{}`", 369 | model_ret_value_debug, 370 | tested_ret_value_debug 371 | ); 372 | } 373 | #[cfg(not(fuzzing_debug))] 374 | if let Outcome::Unequal = outcome { 375 | rutenspitz::panic!("The return values aren't equal"); 376 | } 377 | } 378 | WhichFailed::First => { 379 | // First paniced, see if the second one also does 380 | let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 381 | let _ = tested.#method_name(#(#args),*); 382 | })); 383 | if result.is_ok() { 384 | rutenspitz::panic!("Implementation did not panic while the model did"); 385 | } 386 | } 387 | WhichFailed::Second => { 388 | rutenspitz::panic!("Implementation panicked while the model did not"); 389 | } 390 | } 391 | } 392 | }); 393 | } else { 394 | tokens.extend(quote! { 395 | #pattern => { 396 | let _ = tested.#method_name(#(#args),*); 397 | } 398 | }); 399 | } 400 | } 401 | } 402 | 403 | struct OperationEnum<'s> { 404 | spec: &'s Specification, 405 | } 406 | 407 | impl<'s> quote::ToTokens for OperationEnum<'s> { 408 | #[allow(clippy::cognitive_complexity)] 409 | fn to_tokens(&self, tokens: &mut pm2::TokenStream) { 410 | let lifetimes = &self.spec.lifetimes; 411 | let type_params_with_bounds = &self.spec.type_params; 412 | let type_params: Vec<_> = type_params_with_bounds 413 | .iter() 414 | .map(|tp| tp.ident.clone()) 415 | .collect(); 416 | 417 | let model = &self.spec.model; 418 | let tested = &self.spec.tested; 419 | let variants = &self.spec.methods; 420 | 421 | let comp_method_tests: Vec<_> = self 422 | .spec 423 | .methods 424 | .iter() 425 | .map(|method| MethodTest { 426 | method, 427 | compare: true, 428 | }) 429 | .collect(); 430 | 431 | let method_tests: Vec<_> = self 432 | .spec 433 | .methods 434 | .iter() 435 | .map(|method| MethodTest { 436 | method, 437 | compare: false, 438 | }) 439 | .collect(); 440 | 441 | let format_calls: Vec<_> = self 442 | .spec 443 | .methods 444 | .iter() 445 | .map(|method| { 446 | let args: Vec<_> = method 447 | .inputs 448 | .iter() 449 | .map(|input| match input.passing_mode { 450 | PassingMode::ByValue => "{:?}", 451 | PassingMode::ByRef => "&{:?}", 452 | PassingMode::ByRefMut => "&mut {:?}", 453 | }) 454 | .collect(); 455 | 456 | let method_name = &method.name; 457 | let format_str = format!("v.{}({});", method_name, args.join(", ")); 458 | let keys: Vec<_> = method.inputs.iter().map(|input| &input.name).collect(); 459 | let pattern = if keys.is_empty() { 460 | quote! { Op::#method_name } 461 | } else { 462 | quote! { Op::#method_name { #(#keys),* } } 463 | }; 464 | 465 | quote! { #pattern => 466 | write!(f, #format_str, #(#keys),*) 467 | } 468 | }) 469 | .collect(); 470 | 471 | let post = &self.spec.post; 472 | let pre = &self.spec.pre; 473 | 474 | tokens.extend(quote! { 475 | #[allow(non_camel_case_types)] 476 | #[derive(rutenspitz::derive::Arbitrary, rutenspitz::derive::IntoStaticStr, Clone, Debug, PartialEq)] 477 | pub enum Op<#(#type_params_with_bounds),*> { 478 | #(#variants),* 479 | } 480 | 481 | impl<#(#type_params_with_bounds),*> std::fmt::Display for Op<#(#type_params),*> { 482 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 483 | match self { 484 | #(#format_calls),* 485 | } 486 | } 487 | } 488 | 489 | impl<#(#type_params_with_bounds),*> Op<#(#type_params),*> { 490 | pub fn execute <#(#lifetimes),*> (self, tested: &mut #tested) { 491 | match &self { 492 | #(#method_tests),* 493 | } 494 | } 495 | 496 | pub fn execute_and_compare <#(#lifetimes),*> (self, model: &mut #model, tested: &mut #tested) { 497 | #[cfg(not(fuzzing_debug))] 498 | rutenspitz::lazy_static::initialize(&rutenspitz::NON_DEBUG_PANIC_HOOK); 499 | 500 | let op_name: &'static str = From::from(&self); 501 | #(#pre)* 502 | match &self { 503 | #(#comp_method_tests),* 504 | } 505 | #(#post)* 506 | } 507 | 508 | #[inline(always)] 509 | pub fn append_to_trace(&self, trace: &mut String) { 510 | #[cfg(fuzzing_debug)] 511 | trace.push_str(&format!("{}\n", self.to_string())); 512 | } 513 | } 514 | }); 515 | } 516 | } 517 | 518 | #[proc_macro] 519 | pub fn arbitrary_stateful_operations(input: pm::TokenStream) -> pm::TokenStream { 520 | let parsed_spec = parse_macro_input!(input as Specification); 521 | 522 | let operation_enum = OperationEnum { spec: &parsed_spec }; 523 | 524 | let output = quote! { 525 | mod op { 526 | use super::*; 527 | #operation_enum 528 | } 529 | }; 530 | 531 | output.into() 532 | } 533 | --------------------------------------------------------------------------------