├── .gitignore ├── CODEOWNERS ├── third-party ├── src │ └── lib.rs ├── Cargo.toml ├── third_party.json ├── patches │ ├── cw-plus.patch │ ├── example-helloworld.patch │ └── substrate_client_transaction_pool.patch └── tests │ └── third_party.rs ├── test-fuzz ├── README.md ├── build.rs ├── tests │ └── integration │ │ ├── main.rs │ │ ├── test_fuzz_log.rs │ │ ├── rename.rs │ │ ├── conversion.rs │ │ ├── self_ty_in_mod_name.rs │ │ ├── default.rs │ │ ├── auto_generate.rs │ │ ├── link.rs │ │ ├── serde_format.rs │ │ ├── versions.rs │ │ ├── in_production.rs │ │ └── ci.rs ├── src │ ├── lib.rs │ ├── convert.rs │ └── utils.rs └── Cargo.toml ├── cargo-test-fuzz ├── README.md ├── tests │ ├── integration │ │ ├── main.rs │ │ ├── build.rs │ │ ├── display.rs │ │ ├── fuzz_cast.rs │ │ ├── fuzz_profile.rs │ │ ├── fuzz.rs │ │ ├── fuzz_generic.rs │ │ ├── auto_generate.rs │ │ ├── consolidate.rs │ │ ├── fuzz_parallel.rs │ │ ├── generic_args.rs │ │ └── replay.rs │ └── install.rs ├── src │ └── bin │ │ └── cargo_test_fuzz │ │ ├── main.rs │ │ └── transition.rs └── Cargo.toml ├── macro ├── README.md ├── src │ ├── pat_utils.rs │ ├── ord_type.rs │ └── type_utils.rs └── Cargo.toml ├── internal ├── README.md ├── src │ ├── lib.rs │ ├── serde_format.rs │ └── dirs.rs ├── build.rs └── Cargo.toml ├── runtime ├── README.md ├── Cargo.toml └── src │ ├── traits.rs │ └── lib.rs ├── rustfmt.toml ├── examples ├── tests │ ├── assert.rs │ ├── cast.rs │ ├── profile.rs │ ├── rename.rs │ ├── alloc.rs │ ├── self_ty_in_mod_name.rs │ ├── auto_generate.rs │ ├── mut_ref.rs │ ├── parse_duration.rs │ ├── test_fuzz_impl.rs │ ├── collision.rs │ ├── unserde.rs │ ├── qwerty.rs │ ├── return_type.rs │ ├── manual_leak.rs │ ├── calm_and_panicky.rs │ ├── parallel.rs │ ├── arc.rs │ ├── default.rs │ ├── serde_attr.rs │ ├── lifetime.rs │ ├── debug.rs │ ├── from.rs │ ├── associated_type.rs │ ├── generic.rs │ ├── conversion.rs │ └── serde.rs ├── src │ └── main.rs ├── Cargo.toml └── build.rs ├── clippy.toml ├── testing ├── src │ ├── lib.rs │ ├── retry.rs │ ├── command_ext.rs │ └── examples.rs └── Cargo.toml ├── docs └── crates.dot ├── scripts ├── remove_patch_versions.sh ├── check_CHANGELOG.sh ├── update_versions.sh └── update_patches.sh ├── .github ├── dependabot.yml └── workflows │ ├── update_patches.yml │ ├── release.yml │ └── ci.yml ├── serde_combinators ├── Cargo.toml ├── src │ ├── type_.rs │ ├── ref_.rs │ ├── ref_mut.rs │ ├── mutex.rs │ ├── rw_lock.rs │ └── lib.rs └── tests │ ├── ref_rw_lock.rs │ └── ref_mut_mutex.rs ├── CONTRIBUTING.md ├── dylint.toml └── Cargo.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @smoelius 2 | -------------------------------------------------------------------------------- /third-party/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test-fuzz/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /cargo-test-fuzz/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /macro/README.md: -------------------------------------------------------------------------------- 1 | # test-fuzz-macro 2 | -------------------------------------------------------------------------------- /internal/README.md: -------------------------------------------------------------------------------- 1 | # test-fuzz-internal 2 | -------------------------------------------------------------------------------- /runtime/README.md: -------------------------------------------------------------------------------- 1 | # test-fuzz-runtime 2 | -------------------------------------------------------------------------------- /internal/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod dirs; 2 | 3 | pub mod serde_format; 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | comment_width = 100 2 | format_code_in_doc_comments = true 3 | format_strings = true 4 | style_edition = "2024" 5 | wrap_comments = true 6 | -------------------------------------------------------------------------------- /test-fuzz/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(not(any(feature = "serde_bincode", feature = "serde_postcard")))] 3 | println!("cargo:rustc-cfg=serde_default"); 4 | } 5 | -------------------------------------------------------------------------------- /examples/tests/assert.rs: -------------------------------------------------------------------------------- 1 | #[test_fuzz::test_fuzz] 2 | fn target(x: bool) { 3 | assert!(!x); 4 | } 5 | 6 | #[test] 7 | fn test() { 8 | target(false); 9 | } 10 | -------------------------------------------------------------------------------- /internal/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(not(any(feature = "__serde_bincode", feature = "__serde_postcard")))] 3 | println!("cargo:rustc-cfg=serde_default"); 4 | } 5 | -------------------------------------------------------------------------------- /examples/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | target("Hello, world!"); 3 | } 4 | 5 | #[test_fuzz::test_fuzz(enable_in_production)] 6 | fn target(s: &str) { 7 | println!("{s}"); 8 | } 9 | -------------------------------------------------------------------------------- /examples/tests/cast.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::cast_possible_truncation)] 2 | #[test_fuzz::test_fuzz] 3 | fn target(x: u64) { 4 | let _ = x as u32; 5 | } 6 | 7 | #[test] 8 | fn test() { 9 | target(0); 10 | } 11 | -------------------------------------------------------------------------------- /examples/tests/profile.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/profile.rs")); 2 | 3 | #[test_fuzz::test_fuzz] 4 | fn target(x: bool) { 5 | assert!(!x || PROFILE != "release"); 6 | } 7 | 8 | #[test] 9 | fn test() { 10 | target(false); 11 | } 12 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | mod auto_generate; 2 | mod build; 3 | mod consolidate; 4 | mod display; 5 | mod fuzz; 6 | mod fuzz_cast; 7 | mod fuzz_generic; 8 | mod fuzz_parallel; 9 | mod fuzz_profile; 10 | mod generic_args; 11 | mod replay; 12 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-methods = [ 2 | { path = "assert_cmd::assert::OutputAssertExt::assert", reason = "use `test_fuzz_testing::CommandExt::logged_assert`" }, 3 | { path = "assert_cmd::cmd::Command::assert", reason = "use `test_fuzz_testing::CommandExt::logged_assert`" }, 4 | ] 5 | -------------------------------------------------------------------------------- /examples/tests/rename.rs: -------------------------------------------------------------------------------- 1 | #[test_fuzz::test_fuzz(rename = "bar")] 2 | fn foo() {} 3 | 4 | // smoelius: Building with feature `__bar_fuzz` should produce a name collision. 5 | #[cfg(feature = "__bar_fuzz")] 6 | mod bar_fuzz__ {} 7 | 8 | #[test] 9 | fn test() { 10 | foo(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/tests/alloc.rs: -------------------------------------------------------------------------------- 1 | #[test_fuzz::test_fuzz(no_auto_generate)] 2 | fn target(n: usize) { 3 | let vec = Vec::::with_capacity(n); 4 | // smoelius: Prevent `vec` from being optimized away. 5 | println!("{:p}", &vec); 6 | } 7 | 8 | #[test] 9 | fn test() { 10 | target(0); 11 | } 12 | -------------------------------------------------------------------------------- /testing/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod command_ext; 2 | pub use command_ext::CommandExt; 3 | 4 | pub mod examples; 5 | 6 | mod retry; 7 | pub use crate::retry::*; 8 | 9 | #[ctor::ctor] 10 | fn init() { 11 | internal::dirs::IN_TEST.store(true, std::sync::atomic::Ordering::SeqCst); 12 | 13 | env_logger::init(); 14 | } 15 | -------------------------------------------------------------------------------- /examples/tests/self_ty_in_mod_name.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Deserialize, Serialize)] 4 | struct Struct; 5 | 6 | #[test_fuzz::test_fuzz_impl] 7 | impl Struct { 8 | #[test_fuzz::test_fuzz] 9 | fn target(&self) {} 10 | } 11 | 12 | #[cfg(feature = "__self_ty_conflict")] 13 | mod struct_target_fuzz__ {} 14 | -------------------------------------------------------------------------------- /examples/tests/auto_generate.rs: -------------------------------------------------------------------------------- 1 | mod signed { 2 | #[test_fuzz::test_fuzz] 3 | fn target(x: i64) {} 4 | 5 | #[test] 6 | fn test() { 7 | target(i64::default()); 8 | } 9 | } 10 | 11 | mod unsigned { 12 | #[test_fuzz::test_fuzz] 13 | fn target(x: u64) {} 14 | 15 | #[test] 16 | fn test() { 17 | target(u64::default()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/tests/mut_ref.rs: -------------------------------------------------------------------------------- 1 | #[test_fuzz::test_fuzz] 2 | fn target_slice(xs: &mut [u8]) { 3 | if !xs.is_empty() { 4 | xs[0] = 1; 5 | } 6 | } 7 | 8 | #[test_fuzz::test_fuzz] 9 | fn target_str(s: &mut str) { 10 | s.make_ascii_uppercase(); 11 | } 12 | 13 | #[test] 14 | fn test() { 15 | target_slice(&mut [0]); 16 | target_str(String::from("x").as_mut_str()); 17 | } 18 | -------------------------------------------------------------------------------- /docs/crates.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | "cargo-test-fuzz" -> "internal"; 3 | "cargo-test-fuzz" -> "runtime"; 4 | "cargo-test-fuzz" -> "testing"; 5 | "macro-generated-code" -> "test-fuzz"; 6 | "runtime" -> "internal"; 7 | "test-fuzz" -> "internal"; 8 | "test-fuzz" -> "macro"; 9 | "test-fuzz" -> "runtime"; 10 | "test-fuzz" -> "testing"; 11 | "testing" -> "internal"; 12 | } -------------------------------------------------------------------------------- /test-fuzz/tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | mod auto_generate; 2 | mod ci; 3 | mod conversion; 4 | mod default; 5 | mod in_production; 6 | mod link; 7 | mod rename; 8 | mod self_ty_in_mod_name; 9 | mod serde_format; 10 | mod test_fuzz_log; 11 | mod versions; 12 | 13 | #[ctor::ctor] 14 | fn initialize() { 15 | unsafe { 16 | std::env::set_var("CARGO_TERM_COLOR", "never"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/remove_patch_versions.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # set -x 4 | set -euo pipefail 5 | 6 | if [[ $# -ne 0 ]]; then 7 | echo "$0: expect no arguments" >&2 8 | exit 1 9 | fi 10 | 11 | SCRIPTS="$(dirname "$(realpath "$0")")" 12 | WORKSPACE="$(realpath "$SCRIPTS"/..)" 13 | 14 | cd "$WORKSPACE" 15 | 16 | find . -name Cargo.toml -exec sed -i '/^version\>/!s/"\([0-9]\+\.[0-9]\+\)\.[0-9]\+"/"\1"/' {} \; 17 | -------------------------------------------------------------------------------- /examples/tests/parse_duration.rs: -------------------------------------------------------------------------------- 1 | // smoelius: This example is due to @disconnect3d. 2 | // See: https://rustsec.org/advisories/RUSTSEC-2021-0041.html 3 | 4 | use parse_duration::parse::Error; 5 | use std::time::Duration; 6 | 7 | #[test_fuzz::test_fuzz] 8 | fn parse(input: &str) -> Result { 9 | parse_duration::parse(input) 10 | } 11 | 12 | #[test] 13 | fn test() { 14 | parse("1e5 ns").unwrap(); 15 | } 16 | -------------------------------------------------------------------------------- /examples/tests/test_fuzz_impl.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Deserialize, Serialize)] 4 | struct Struct; 5 | 6 | #[test_fuzz::test_fuzz_impl] 7 | impl Struct { 8 | #[test_fuzz::test_fuzz] 9 | fn foo(&self) {} 10 | 11 | #[test_fuzz::test_fuzz] 12 | fn bar(&mut self) {} 13 | } 14 | 15 | #[test] 16 | fn foo() { 17 | Struct.foo(); 18 | } 19 | 20 | #[test] 21 | fn bar() { 22 | Struct.bar(); 23 | } 24 | -------------------------------------------------------------------------------- /examples/tests/collision.rs: -------------------------------------------------------------------------------- 1 | // smoelius: https://github.com/trailofbits/test-fuzz/issues/520 2 | // Previously, `test_fuzz` would generate a module whose name was the target's name appended with 3 | // `_fuzz`. If the target's name was `test`, a collision would result between `test_fuzz`'s package 4 | // name and the generated module name. `test_fuzz` now avoids this problem by appending `_fuzz__` 5 | // instead of `_fuzz`. 6 | #[test_fuzz::test_fuzz] 7 | fn test() {} 8 | -------------------------------------------------------------------------------- /examples/tests/unserde.rs: -------------------------------------------------------------------------------- 1 | // smoelius: `Struct` is *not* serializable/deserializable. 2 | struct Struct; 3 | 4 | #[test_fuzz::test_fuzz(only_generic_args)] 5 | fn target(_: &T) {} 6 | 7 | #[test_fuzz::test_fuzz(enable_in_production, only_generic_args)] 8 | fn target_in_production(_: &T) {} 9 | 10 | #[test] 11 | fn test() { 12 | target(&Struct); 13 | } 14 | 15 | #[test] 16 | fn test_in_production() { 17 | target_in_production(&Struct); 18 | } 19 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/build.rs: -------------------------------------------------------------------------------- 1 | use testing::{CommandExt, examples}; 2 | 3 | #[test] 4 | fn build() { 5 | examples::test_fuzz_all() 6 | .unwrap() 7 | .args(["--no-run"]) 8 | .logged_assert() 9 | .success(); 10 | } 11 | 12 | #[test] 13 | fn build_persistent() { 14 | examples::test_fuzz_all() 15 | .unwrap() 16 | .args(["--no-run", "--persistent"]) 17 | .logged_assert() 18 | .success(); 19 | } 20 | -------------------------------------------------------------------------------- /scripts/check_CHANGELOG.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # set -x 4 | set -euo pipefail 5 | 6 | if [[ $# -ne 1 ]]; then 7 | echo "$0: expect one argument: 'github.ref'" >&2 8 | exit 1 9 | fi 10 | 11 | REF="$1" 12 | 13 | if [[ ${REF::11} != 'refs/tags/v' ]]; then 14 | echo "$0: expect \`github.ref\` to start with \`refs/tags/v\`: $REF" >&2 15 | exit 1 16 | fi 17 | 18 | VERSION="${REF:11}" 19 | 20 | cd "$(dirname "$0")"/.. 21 | 22 | grep "^## $VERSION$" CHANGELOG.md 23 | -------------------------------------------------------------------------------- /examples/tests/qwerty.rs: -------------------------------------------------------------------------------- 1 | #[test_fuzz::test_fuzz] 2 | fn target(data: &str) { 3 | assert!( 4 | !(data.len() == 6 5 | && data.as_bytes()[0] == b'q' 6 | && data.as_bytes()[1] == b'w' 7 | && data.as_bytes()[2] == b'e' 8 | && data.as_bytes()[3] == b'r' 9 | && data.as_bytes()[4] == b't' 10 | && data.as_bytes()[5] == b'y') 11 | ); 12 | } 13 | 14 | #[test] 15 | fn test() { 16 | target("asdfgh"); 17 | } 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | day: thursday 8 | time: "03:00" 9 | open-pull-requests-limit: 10 10 | 11 | - package-ecosystem: cargo 12 | directory: / 13 | groups: 14 | rust: 15 | patterns: 16 | - "*" 17 | schedule: 18 | interval: weekly 19 | day: thursday 20 | time: "03:00" 21 | open-pull-requests-limit: 10 22 | -------------------------------------------------------------------------------- /examples/tests/return_type.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Deserialize, Serialize)] 4 | struct Swap(T, U); 5 | 6 | #[test_fuzz::test_fuzz_impl] 7 | impl Swap 8 | where 9 | T: Clone + Serialize, 10 | U: Clone + Serialize, 11 | { 12 | #[test_fuzz::test_fuzz(impl_generic_args = "(), bool")] 13 | fn swap(self) -> Swap { 14 | Swap(self.1, self.0) 15 | } 16 | } 17 | 18 | #[test] 19 | fn test() { 20 | let _ = Swap((), false).swap(); 21 | } 22 | -------------------------------------------------------------------------------- /serde_combinators/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serde_combinators" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | description = "Combinators for producing Serde serialize and deserialize functions" 7 | 8 | authors = ["Samuel E. Moelius III "] 9 | license = "MIT OR Apache-2.0" 10 | repository = "https://github.com/trailofbits/test-fuzz" 11 | 12 | [dependencies] 13 | serde = { workspace = true } 14 | 15 | [dev-dependencies] 16 | serde_assert = { workspace = true } 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /testing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-fuzz-testing" 3 | version = "7.2.5" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | anyhow = { workspace = true } 9 | assert_cmd = { workspace = true } 10 | cargo_metadata = { workspace = true } 11 | ctor = { workspace = true } 12 | env_logger = { workspace = true } 13 | log = { workspace = true } 14 | strip-ansi-escapes = { workspace = true } 15 | subprocess = { workspace = true } 16 | 17 | internal = { workspace = true } 18 | 19 | [lints] 20 | workspace = true 21 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-fuzz-examples" 3 | version = "7.2.5" 4 | edition = "2024" 5 | publish = false 6 | 7 | [[bin]] 8 | name = "hello-world" 9 | path = "src/main.rs" 10 | 11 | [dependencies] 12 | serde = { workspace = true } 13 | test-fuzz = { workspace = true } 14 | 15 | [dev-dependencies] 16 | parse_duration = { workspace = true } 17 | serde_json = { workspace = true } 18 | 19 | [features] 20 | __bar_fuzz = [] 21 | __inapplicable_conversion = [] 22 | __self_ty_conflict = [] 23 | 24 | [lints] 25 | workspace = true 26 | -------------------------------------------------------------------------------- /runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-fuzz-runtime" 3 | version = "7.2.5" 4 | edition = "2024" 5 | 6 | description = "test-fuzz-runtime" 7 | 8 | authors = ["Samuel E. Moelius III "] 9 | license = "AGPL-3.0 WITH mif-exception" 10 | repository = "https://github.com/trailofbits/test-fuzz" 11 | 12 | [dependencies] 13 | hex = { workspace = true } 14 | num-traits = { workspace = true } 15 | serde = { workspace = true } 16 | sha1 = { workspace = true } 17 | 18 | internal = { workspace = true } 19 | 20 | [lints] 21 | workspace = true 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | WIP 2 | 3 | ## Tests 4 | 5 | Tests in this repository reside in three places: 6 | 7 | - `cargo-test-fuzz/tests` - Tests that exercise `cargo-test-fuzz`. Files should be named for the `cargo-test-fuzz` feature or function they exercise. 8 | - `test-fuzz/tests` - Tests that exercise `test-fuzz`, but not `cargo-test-fuzz`. Files should be named for the `test-fuzz` feature or function they exercise. 9 | - `examples/tests` - Targets of the previous two types of tests. Files should be named for the Rust feature, function, library, or trait they exercise. 10 | -------------------------------------------------------------------------------- /examples/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::{var, var_os}, 3 | fs::OpenOptions, 4 | io::Write, 5 | path::PathBuf, 6 | }; 7 | 8 | fn main() { 9 | let profile = var("PROFILE").unwrap(); 10 | let out_dir = var_os("OUT_DIR").unwrap(); 11 | let path_buf = PathBuf::from(out_dir).join("profile.rs"); 12 | let mut file = OpenOptions::new() 13 | .create(true) 14 | .write(true) 15 | .truncate(true) 16 | .open(path_buf) 17 | .unwrap(); 18 | writeln!(file, r#"const PROFILE: &str = "{profile}";"#).unwrap(); 19 | } 20 | -------------------------------------------------------------------------------- /examples/tests/manual_leak.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Debug, Deserialize, Serialize)] 4 | pub struct WrappedKey<'key> { 5 | #[serde(with = "test_fuzz::serde_ref")] 6 | key: &'key [u8; 32], 7 | } 8 | 9 | #[test_fuzz::test_fuzz_impl] 10 | impl<'key> WrappedKey<'key> { 11 | #[test_fuzz::test_fuzz] 12 | fn dump(&self) { 13 | println!("{:?}", self.key); 14 | } 15 | } 16 | 17 | #[test] 18 | fn test_dump() { 19 | let key: [u8; 32] = [1; 32]; 20 | let wrapped_key = WrappedKey { key: &key }; 21 | wrapped_key.dump(); 22 | } 23 | -------------------------------------------------------------------------------- /examples/tests/calm_and_panicky.rs: -------------------------------------------------------------------------------- 1 | //! The function [`calm`] intentionally returns immediately. The function [`panicky`] intentionally 2 | //! panics. 3 | 4 | #[derive(Clone, serde::Deserialize, serde::Serialize)] 5 | struct Calm(bool); 6 | 7 | #[derive(Clone, serde::Deserialize, serde::Serialize)] 8 | struct Panicky(bool); 9 | 10 | #[test_fuzz::test_fuzz] 11 | fn calm(_calm: Calm) {} 12 | 13 | #[test_fuzz::test_fuzz] 14 | fn panicky(_panicky: Panicky) { 15 | panic!(); 16 | } 17 | 18 | #[test] 19 | #[should_panic] 20 | fn test() { 21 | calm(Calm(false)); 22 | panicky(Panicky(false)); 23 | } 24 | -------------------------------------------------------------------------------- /examples/tests/parallel.rs: -------------------------------------------------------------------------------- 1 | #[test_fuzz::test_fuzz] 2 | fn target_0(x: bool) {} 3 | 4 | #[test_fuzz::test_fuzz] 5 | fn target_1(x: bool) {} 6 | 7 | #[test_fuzz::test_fuzz] 8 | fn target_2(x: bool) {} 9 | 10 | #[test_fuzz::test_fuzz] 11 | fn target_3(x: bool) { 12 | assert!(!x); 13 | } 14 | 15 | #[test_fuzz::test_fuzz] 16 | fn target_4(x: bool) {} 17 | 18 | #[test_fuzz::test_fuzz] 19 | fn target_5(x: bool) {} 20 | 21 | #[test] 22 | fn test() { 23 | target_0(false); 24 | target_1(false); 25 | target_2(false); 26 | target_3(false); 27 | target_4(false); 28 | target_5(false); 29 | } 30 | -------------------------------------------------------------------------------- /examples/tests/arc.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | thread::{current, spawn}, 4 | }; 5 | 6 | #[test] 7 | fn test() { 8 | let mut set = HashSet::new(); 9 | for i in 0..32 { 10 | set.insert(i); 11 | } 12 | 13 | let arc = std::sync::Arc::new(set); 14 | 15 | for i in 0..32 { 16 | let arc = arc.clone(); 17 | spawn(move || { 18 | target(arc, i); 19 | }); 20 | } 21 | } 22 | 23 | #[test_fuzz::test_fuzz] 24 | fn target(arc: std::sync::Arc>, i: usize) { 25 | println!("{:?}: {:?}", current().id(), arc.iter().nth(i)); 26 | } 27 | -------------------------------------------------------------------------------- /macro/src/pat_utils.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | Ident, Pat, PatIdent, 3 | visit::{Visit, visit_pat}, 4 | }; 5 | 6 | struct PatVisitor<'a> { 7 | idents: Vec<&'a Ident>, 8 | } 9 | 10 | impl<'a> Visit<'a> for PatVisitor<'a> { 11 | fn visit_pat(&mut self, pat: &'a Pat) { 12 | if let Pat::Ident(PatIdent { ident, .. }) = pat { 13 | self.idents.push(ident); 14 | } 15 | visit_pat(self, pat); 16 | } 17 | } 18 | 19 | pub fn pat_idents(pat: &Pat) -> Vec<&Ident> { 20 | let mut visitor = PatVisitor { idents: Vec::new() }; 21 | visitor.visit_pat(pat); 22 | visitor.idents 23 | } 24 | -------------------------------------------------------------------------------- /examples/tests/default.rs: -------------------------------------------------------------------------------- 1 | mod no_default { 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Deserialize, Serialize)] 5 | struct Struct(bool); 6 | 7 | #[test_fuzz::test_fuzz] 8 | fn target(s: &Struct) {} 9 | 10 | #[test] 11 | fn test() { 12 | target(&Struct(true)); 13 | } 14 | } 15 | 16 | mod default { 17 | use serde::{Deserialize, Serialize}; 18 | 19 | #[derive(Clone, Default, Deserialize, Serialize)] 20 | struct Struct(bool); 21 | 22 | #[test_fuzz::test_fuzz] 23 | fn target(s: &Struct) {} 24 | 25 | #[test] 26 | fn test() { 27 | target(&Struct(true)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/tests/serde_attr.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(dylint_lib = "general", allow(crate_wide_allow))] 2 | #![allow(unused)] 3 | 4 | use std::sync::Mutex; 5 | 6 | // Traits `serde::Serialize` and `serde::Deserialize` cannot be derived for `Context` because it 7 | // contains a `Mutex`. 8 | #[derive(Default)] 9 | struct Context { 10 | lock: Mutex<()>, 11 | } 12 | 13 | impl Clone for Context { 14 | fn clone(&self) -> Self { 15 | Self { 16 | lock: Mutex::new(()), 17 | } 18 | } 19 | } 20 | 21 | #[test_fuzz::test_fuzz] 22 | fn target(#[serde(skip)] context: Context, x: i32) { 23 | assert!(x >= 0); 24 | } 25 | 26 | #[test] 27 | fn test() { 28 | target(Context::default(), 0); 29 | } 30 | -------------------------------------------------------------------------------- /macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-fuzz-macro" 3 | version = "7.2.5" 4 | edition = "2024" 5 | 6 | description = "test-fuzz-macro" 7 | 8 | authors = ["Samuel E. Moelius III "] 9 | license = "AGPL-3.0 WITH mif-exception" 10 | repository = "https://github.com/trailofbits/test-fuzz" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | darling = { workspace = true } 17 | heck = { workspace = true } 18 | itertools = { workspace = true } 19 | prettyplease = { workspace = true } 20 | proc-macro2 = { workspace = true } 21 | quote = { workspace = true } 22 | syn = { workspace = true } 23 | 24 | [features] 25 | __cast_checks = [] 26 | __persistent = [] 27 | 28 | [lints] 29 | workspace = true 30 | -------------------------------------------------------------------------------- /scripts/update_versions.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -euo pipefail 4 | 5 | if [[ $# -ne 1 ]]; then 6 | echo "$0: expect one argument: version" >&2 7 | exit 1 8 | fi 9 | 10 | set -x 11 | 12 | SCRIPTS="$(dirname "$(realpath "$0")")" 13 | WORKSPACE="$(realpath "$SCRIPTS"/..)" 14 | 15 | cd "$WORKSPACE" 16 | 17 | VERSION="version = \"$1\"" 18 | 19 | find . -name Cargo.toml | 20 | grep -v 'serde_combinators/Cargo.toml' | 21 | xargs -n 1 sed -i "{ 22 | s/^version = \"[^\"]*\"$/$VERSION/ 23 | }" 24 | 25 | REQ="${VERSION/\"/\"=}" 26 | 27 | sed -i "/^test-fuzz/{ 28 | s/^\(.*\)\(n: u64, operation: O) -> Result 5 | where 6 | O: Fn() -> Result, 7 | T: Display, 8 | E: Display, 9 | { 10 | assert!(n >= 1); 11 | 12 | for i in 1..=n { 13 | let result = operation(); 14 | match &result { 15 | Ok(value) => { 16 | info!("{value}"); 17 | return result; 18 | } 19 | Err(error) if i < n => { 20 | warn!("{error}"); 21 | } 22 | Err(error) => { 23 | error!("{error}"); 24 | return result; 25 | } 26 | } 27 | } 28 | 29 | unreachable!(); 30 | } 31 | -------------------------------------------------------------------------------- /test-fuzz/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use runtime; 2 | pub use test_fuzz_macro::{test_fuzz, test_fuzz_impl}; 3 | 4 | // smoelius: Re-export afl so that test-fuzz clients do not need to add it to their Cargo.toml 5 | // files. 6 | #[cfg(feature = "__persistent")] 7 | pub use afl; 8 | 9 | // smoelius: Do the same for `cast_checks`. 10 | #[cfg(feature = "cast_checks")] 11 | pub use cast_checks; 12 | 13 | // smoelius: Unfortunately, the same trick doesn't work for serde. 14 | // https://github.com/serde-rs/serde/issues/1465 15 | 16 | pub use internal::serde_format; 17 | 18 | mod utils; 19 | pub use utils::{ 20 | deserialize_ref, deserialize_ref_mut, serde_ref, serde_ref_mut, serialize_ref, 21 | serialize_ref_mut, 22 | }; 23 | 24 | mod convert; 25 | pub use convert::{FromRef, Into}; 26 | -------------------------------------------------------------------------------- /serde_combinators/src/type_.rs: -------------------------------------------------------------------------------- 1 | use super::{DeserializeWith, SerializeWith}; 2 | use serde::{Deserializer, Serialize, Serializer, de::DeserializeOwned}; 3 | use std::marker::PhantomData; 4 | 5 | pub struct Type(PhantomData); 6 | 7 | impl SerializeWith for Type 8 | where 9 | T: Serialize, 10 | { 11 | type T = T; 12 | 13 | fn serialize(value: &T, serializer: S) -> Result 14 | where 15 | S: Serializer, 16 | { 17 | T::serialize(value, serializer) 18 | } 19 | } 20 | 21 | impl DeserializeWith for Type 22 | where 23 | T: DeserializeOwned, 24 | { 25 | type T = T; 26 | 27 | fn deserialize<'de, D>(deserializer: D) -> Result 28 | where 29 | D: Deserializer<'de>, 30 | { 31 | T::deserialize(deserializer) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/test_fuzz_log.rs: -------------------------------------------------------------------------------- 1 | use predicates::prelude::*; 2 | use std::process::Command; 3 | use testing::{CommandExt, examples::MANIFEST_PATH}; 4 | 5 | // smoelius: This test will fail if run twice because the target will have already been built. 6 | #[test] 7 | fn test_fuzz_log() { 8 | Command::new("cargo") 9 | .env("TEST_FUZZ_LOG", "1") 10 | .args([ 11 | "test", 12 | "--manifest-path", 13 | &MANIFEST_PATH, 14 | "--no-run", 15 | "--features", 16 | &("test-fuzz/".to_owned() + test_fuzz::serde_format::as_feature()), 17 | "--test=parse_duration", 18 | ]) 19 | .logged_assert() 20 | .success() 21 | .stdout(predicate::str::is_match(r"(?m)^#\[cfg\(test\)\]\nmod parse_fuzz__ \{$").unwrap()); 22 | } 23 | -------------------------------------------------------------------------------- /testing/src/command_ext.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::assert::Assert; 2 | use log::debug; 3 | use std::process::{Command, Output}; 4 | 5 | pub trait CommandExt { 6 | fn logged_assert(&mut self) -> Assert; 7 | } 8 | 9 | impl CommandExt for Command { 10 | fn logged_assert(&mut self) -> Assert { 11 | debug!("{self:?}"); 12 | let output = self.output().unwrap(); 13 | Assert::new(output_stripped_of_ansi_escapes(output)) 14 | } 15 | } 16 | 17 | fn output_stripped_of_ansi_escapes(output: Output) -> Output { 18 | #[allow(clippy::disallowed_methods)] 19 | let Output { 20 | status, 21 | stdout, 22 | stderr, 23 | } = output; 24 | Output { 25 | status, 26 | stdout: strip_ansi_escapes::strip(stdout), 27 | stderr: strip_ansi_escapes::strip(stderr), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/tests/lifetime.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 2 | 3 | #[derive(Debug)] 4 | struct Foo; 5 | 6 | #[derive(Clone)] 7 | struct Bar<'a>(&'a Foo); 8 | 9 | impl Serialize for Bar<'_> { 10 | fn serialize(&self, serializer: S) -> Result 11 | where 12 | S: Serializer, 13 | { 14 | ().serialize(serializer) 15 | } 16 | } 17 | 18 | impl<'de> Deserialize<'de> for Bar<'_> { 19 | fn deserialize(deserializer: D) -> Result 20 | where 21 | D: Deserializer<'de>, 22 | { 23 | <()>::deserialize(deserializer).map(|()| Bar(&Foo)) 24 | } 25 | } 26 | 27 | #[test_fuzz::test_fuzz] 28 | fn target<'a>(bar: Bar<'a>) { 29 | println!("{:?}", bar.0); 30 | } 31 | 32 | #[test] 33 | fn test() { 34 | target(Bar(&Foo)); 35 | } 36 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/rename.rs: -------------------------------------------------------------------------------- 1 | use predicates::prelude::*; 2 | use std::process::Command; 3 | use testing::{CommandExt, examples::MANIFEST_PATH}; 4 | 5 | #[test] 6 | fn rename() { 7 | let mut command = test(); 8 | 9 | command.logged_assert().success(); 10 | 11 | command 12 | .args(["--features", "__bar_fuzz"]) 13 | .logged_assert() 14 | .failure() 15 | .stderr(predicate::str::contains( 16 | "the name `bar_fuzz__` is defined multiple times", 17 | )); 18 | } 19 | 20 | fn test() -> Command { 21 | let mut command = Command::new("cargo"); 22 | command.args([ 23 | "test", 24 | "--manifest-path", 25 | &MANIFEST_PATH, 26 | "--no-run", 27 | "--features", 28 | &("test-fuzz/".to_owned() + test_fuzz::serde_format::as_feature()), 29 | ]); 30 | command 31 | } 32 | -------------------------------------------------------------------------------- /third-party/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "third-party" 3 | version = "7.2.5" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | 9 | [dev-dependencies] 10 | assert_cmd = { workspace = true } 11 | bitflags = { workspace = true } 12 | cargo_metadata = { workspace = true } 13 | log = { workspace = true } 14 | option_set = { workspace = true } 15 | predicates = { workspace = true } 16 | regex = { workspace = true } 17 | rustc_version = { workspace = true } 18 | serde = { workspace = true } 19 | serde_json = { workspace = true } 20 | tempfile = { workspace = true } 21 | 22 | # smoelius: `test-fuzz` serves as a convenient mechanism for ci.yml to specify the serde format. 23 | # Beyond that, it is not really needed by the `third-party` package. 24 | test-fuzz = { workspace = true } 25 | testing = { workspace = true } 26 | 27 | [features] 28 | test-third-party-full = [] 29 | 30 | [lints] 31 | workspace = true 32 | -------------------------------------------------------------------------------- /examples/tests/debug.rs: -------------------------------------------------------------------------------- 1 | mod crash { 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt::{Debug, Formatter, Result}; 4 | 5 | #[derive(Clone, Default, Deserialize, Serialize)] 6 | struct Struct; 7 | 8 | impl Debug for Struct { 9 | fn fmt(&self, _f: &mut Formatter<'_>) -> Result { 10 | panic!("bug"); 11 | } 12 | } 13 | 14 | #[test_fuzz::test_fuzz] 15 | fn target(s: &Struct) {} 16 | } 17 | 18 | mod hang { 19 | use serde::{Deserialize, Serialize}; 20 | use std::fmt::{Debug, Formatter, Result}; 21 | 22 | #[derive(Clone, Default, Deserialize, Serialize)] 23 | struct Struct; 24 | 25 | impl Debug for Struct { 26 | fn fmt(&self, _f: &mut Formatter<'_>) -> Result { 27 | #[allow(clippy::empty_loop)] 28 | loop {} 29 | } 30 | } 31 | 32 | #[test_fuzz::test_fuzz] 33 | fn target(s: &Struct) {} 34 | } 35 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/conversion.rs: -------------------------------------------------------------------------------- 1 | use predicates::prelude::*; 2 | use std::process::Command; 3 | use testing::{CommandExt, examples::MANIFEST_PATH}; 4 | 5 | #[test] 6 | fn conversion() { 7 | let mut command = test(); 8 | 9 | command.logged_assert().success(); 10 | 11 | command 12 | .args(["--features", "__inapplicable_conversion"]) 13 | .logged_assert() 14 | .failure() 15 | .stderr(predicate::str::is_match(r#"(?m)\bConversion "Y" -> "Z" does not apply to the following candidates: \{\s*"X",\s*}$"#).unwrap()); 16 | } 17 | 18 | fn test() -> Command { 19 | let mut command = Command::new("cargo"); 20 | command.env("CARGO_TERM_COLOR", "never"); 21 | command.args([ 22 | "test", 23 | "--manifest-path", 24 | &MANIFEST_PATH, 25 | "--no-run", 26 | "--features", 27 | &("test-fuzz/".to_owned() + test_fuzz::serde_format::as_feature()), 28 | ]); 29 | command 30 | } 31 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/self_ty_in_mod_name.rs: -------------------------------------------------------------------------------- 1 | use predicates::prelude::*; 2 | use std::process::Command; 3 | use testing::{CommandExt, examples::MANIFEST_PATH}; 4 | 5 | #[test] 6 | fn success() { 7 | let mut command = test(); 8 | 9 | command.logged_assert().success(); 10 | } 11 | 12 | #[test] 13 | fn self_ty_conflict() { 14 | let mut command = test(); 15 | 16 | command 17 | .args(["--features=__self_ty_conflict"]) 18 | .logged_assert() 19 | .failure() 20 | .stderr(predicate::str::contains( 21 | "the name `struct_target_fuzz__` is defined multiple times", 22 | )); 23 | } 24 | 25 | fn test() -> Command { 26 | let mut command = Command::new("cargo"); 27 | command.args([ 28 | "test", 29 | "--manifest-path", 30 | &MANIFEST_PATH, 31 | "--no-run", 32 | "--features", 33 | &("test-fuzz/".to_owned() + test_fuzz::serde_format::as_feature()), 34 | ]); 35 | command 36 | } 37 | -------------------------------------------------------------------------------- /cargo-test-fuzz/src/bin/cargo_test_fuzz/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cargo_test_fuzz::{Object, TestFuzz, run}; 3 | use std::env; 4 | use std::ffi::OsString; 5 | 6 | mod transition; 7 | use transition::cargo_test_fuzz; 8 | 9 | pub fn main() -> Result<()> { 10 | env_logger::init(); 11 | 12 | let args: Vec<_> = env::args().map(OsString::from).collect(); 13 | 14 | cargo_test_fuzz(&args) 15 | } 16 | 17 | #[cfg(test)] 18 | mod tests { 19 | use anyhow::Result; 20 | 21 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 22 | #[test] 23 | fn build_with_target() { 24 | #[allow(clippy::unwrap_used)] 25 | cargo_test_fuzz(&["--no-run", "target"]).unwrap(); 26 | } 27 | 28 | fn cargo_test_fuzz(args: &[&str]) -> Result<()> { 29 | let mut cargo_test_fuzz_args = vec!["cargo-test-fuzz", "test-fuzz"]; 30 | cargo_test_fuzz_args.extend_from_slice(args); 31 | super::cargo_test_fuzz(&cargo_test_fuzz_args) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /macro/src/ord_type.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | use std::cmp::Ordering; 3 | use syn::Type; 4 | 5 | #[derive(Clone)] 6 | pub struct OrdType(pub Type); 7 | 8 | impl std::fmt::Display for OrdType { 9 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 10 | self.0.to_token_stream().fmt(f) 11 | } 12 | } 13 | 14 | impl std::fmt::Debug for OrdType { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | self.to_string().fmt(f) 17 | } 18 | } 19 | 20 | impl Ord for OrdType { 21 | fn cmp(&self, other: &Self) -> Ordering { 22 | ::cmp(&self.to_string(), &other.to_string()) 23 | } 24 | } 25 | 26 | impl PartialOrd for OrdType { 27 | fn partial_cmp(&self, other: &Self) -> Option { 28 | Some(self.cmp(other)) 29 | } 30 | } 31 | 32 | impl Eq for OrdType {} 33 | 34 | impl PartialEq for OrdType { 35 | fn eq(&self, other: &Self) -> bool { 36 | ::eq(&self.to_string(), &other.to_string()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scripts/update_patches.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # set -x 4 | set -euo pipefail 5 | 6 | if [[ $# -ne 0 ]]; then 7 | echo "$0: expect no arguments" >&2 8 | exit 1 9 | fi 10 | 11 | # smoelius: This should match `third-party/tests/third_party.rs`. 12 | LINES_OF_CONTEXT=2 13 | 14 | DIR="$(dirname "$(realpath "$0")")/.." 15 | 16 | cd "$DIR" 17 | 18 | paste <(jq -r .[].url third-party/third_party.json) <(jq -r .[].rev third-party/third_party.json) <(jq -r .[].patch third-party/third_party.json) | 19 | while read -r URL REV_OLD PATCH; do 20 | pushd "$(mktemp -d)" 21 | 22 | git clone --depth 1 "$URL" . 23 | 24 | git apply --reject "$DIR"/third-party/patches/"$PATCH" || true 25 | 26 | find "$PWD" -name '*.rej' 27 | 28 | git diff --unified="$LINES_OF_CONTEXT" > "$DIR"/third-party/patches/"$PATCH" 29 | 30 | # smoelius: Update third_party.json with the revision that was just diffed against. 31 | 32 | REV_NEW="$(git rev-parse HEAD)" 33 | 34 | sed -i "s/\"$REV_OLD\"/\"$REV_NEW\"/" "$DIR"/third-party/third_party.json 35 | 36 | popd 37 | done 38 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/default.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::corpus_directory_from_target; 2 | use std::fs::{read_dir, remove_dir_all}; 3 | use testing::{CommandExt, examples}; 4 | 5 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 6 | #[test] 7 | fn no_default() { 8 | test("no_default", 0); 9 | } 10 | 11 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 12 | #[test] 13 | fn default() { 14 | test("default", 1); 15 | } 16 | 17 | fn test(name: &str, n: usize) { 18 | let corpus = corpus_directory_from_target("default", &format!("{name}::target")); 19 | 20 | // smoelius: `corpus` is distinct for all tests. So there is no race here. 21 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 22 | remove_dir_all(&corpus).unwrap_or_default(); 23 | 24 | examples::test("default", &format!("{name}::target_fuzz__::auto_generate")) 25 | .unwrap() 26 | .logged_assert() 27 | .success(); 28 | 29 | assert_eq!(read_dir(corpus).map(Iterator::count).unwrap_or_default(), n); 30 | } 31 | -------------------------------------------------------------------------------- /internal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-fuzz-internal" 3 | version = "7.2.5" 4 | edition = "2024" 5 | 6 | description = "test-fuzz-internal" 7 | 8 | authors = ["Samuel E. Moelius III "] 9 | license = "AGPL-3.0 WITH mif-exception" 10 | repository = "https://github.com/trailofbits/test-fuzz" 11 | 12 | # smoelius: https://github.com/rust-lang/cargo/issues/1839 13 | # Because of the above issue, the crate for the default format (bincode) must be included regardless 14 | # of whether it is selected. A test-fuzz test (`link`) verifies that the crate's code is not linked 15 | # in when another format is selected. 16 | 17 | [dependencies] 18 | cargo_metadata = { workspace = true } 19 | serde = { workspace = true } 20 | 21 | # smoelius: Serde formats 22 | bincode = { version = "2.0", features = ["serde"] } 23 | postcard = { version = "1.1", features = ["use-std"], optional = true } 24 | 25 | [features] 26 | __serde_bincode = [] 27 | __serde_postcard = ["postcard"] 28 | 29 | [lints] 30 | workspace = true 31 | 32 | [package.metadata.cargo-udeps.ignore] 33 | normal = ["bincode"] 34 | -------------------------------------------------------------------------------- /serde_combinators/src/ref_.rs: -------------------------------------------------------------------------------- 1 | use super::{DeserializeWith, SerializeWith, compose_deserialize, compose_serialize}; 2 | use serde::{Deserializer, Serializer}; 3 | use std::marker::PhantomData; 4 | 5 | pub struct RefF<'a, W>(PhantomData<&'a W>); 6 | 7 | impl<'a, W> SerializeWith for RefF<'a, W> 8 | where 9 | W: SerializeWith, 10 | { 11 | type T = &'a W::T; 12 | 13 | fn serialize(value: &Self::T, serializer: S) -> Result 14 | where 15 | S: Serializer, 16 | { 17 | compose_serialize( 18 | |value, serializer, serialize| serialize(value, serializer), 19 | W::serialize, 20 | )(value, serializer) 21 | } 22 | } 23 | 24 | impl<'a, W> DeserializeWith for RefF<'a, W> 25 | where 26 | W: DeserializeWith, 27 | { 28 | type T = &'a W::T; 29 | 30 | fn deserialize<'de, D>(deserializer: D) -> Result 31 | where 32 | D: Deserializer<'de>, 33 | { 34 | compose_deserialize(W::deserialize, |value| Ok(Box::leak(Box::new(value)) as &_))( 35 | deserializer, 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/auto_generate.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::corpus_directory_from_target; 2 | use std::fs::{read_dir, remove_dir_all}; 3 | use testing::{CommandExt, examples}; 4 | 5 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 6 | #[test] 7 | fn signed() { 8 | test("signed", 6); 9 | } 10 | 11 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 12 | #[test] 13 | fn unsigned() { 14 | test("unsigned", 6); 15 | } 16 | 17 | fn test(name: &str, n: usize) { 18 | let corpus = corpus_directory_from_target("auto_generate", &format!("{name}::target")); 19 | 20 | // smoelius: `corpus` is distinct for all tests. So there is no race here. 21 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 22 | remove_dir_all(&corpus).unwrap_or_default(); 23 | 24 | examples::test( 25 | "auto_generate", 26 | &format!("{name}::target_fuzz__::auto_generate"), 27 | ) 28 | .unwrap() 29 | .logged_assert() 30 | .success(); 31 | 32 | assert_eq!(read_dir(corpus).map(Iterator::count).unwrap_or_default(), n); 33 | } 34 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/link.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::target_directory; 2 | use predicates::prelude::*; 3 | use std::process::Command; 4 | use testing::{CommandExt, examples::MANIFEST_PATH}; 5 | 6 | const SERDE_DEFAULT: &str = "bincode"; 7 | 8 | #[test] 9 | fn link() { 10 | Command::new("cargo") 11 | .args([ 12 | "build", 13 | "--manifest-path", 14 | &MANIFEST_PATH, 15 | "--features", 16 | &("test-fuzz/".to_owned() + test_fuzz::serde_format::as_feature()), 17 | ]) 18 | .logged_assert() 19 | .success(); 20 | 21 | let pred = predicate::str::contains(SERDE_DEFAULT); 22 | 23 | #[cfg(not(any(serde_default, feature = "serde_bincode")))] 24 | let pred = pred.not(); 25 | 26 | // smoelius: https://stackoverflow.com/questions/7219845/difference-between-nm-and-objdump 27 | Command::new("nm") 28 | .args([target_directory(false) 29 | .join("debug/hello-world") 30 | .to_string_lossy() 31 | .to_string()]) 32 | .logged_assert() 33 | .success() 34 | .stdout(pred); 35 | } 36 | -------------------------------------------------------------------------------- /serde_combinators/src/ref_mut.rs: -------------------------------------------------------------------------------- 1 | use super::{DeserializeWith, SerializeWith, compose_deserialize, compose_serialize}; 2 | use serde::{Deserializer, Serializer}; 3 | use std::marker::PhantomData; 4 | 5 | pub struct RefMutF<'a, W>(PhantomData<&'a mut W>); 6 | 7 | impl<'a, W> SerializeWith for RefMutF<'a, W> 8 | where 9 | W: SerializeWith, 10 | { 11 | type T = &'a mut W::T; 12 | 13 | fn serialize(value: &Self::T, serializer: S) -> Result 14 | where 15 | S: Serializer, 16 | { 17 | compose_serialize( 18 | |value, serializer, serialize| serialize(value, serializer), 19 | W::serialize, 20 | )(value, serializer) 21 | } 22 | } 23 | 24 | impl<'a, W> DeserializeWith for RefMutF<'a, W> 25 | where 26 | W: DeserializeWith, 27 | { 28 | type T = &'a mut W::T; 29 | 30 | fn deserialize<'de, D>(deserializer: D) -> Result 31 | where 32 | D: Deserializer<'de>, 33 | { 34 | compose_deserialize(W::deserialize, |value| { 35 | Ok(Box::leak(Box::new(value)) as &mut _) 36 | })(deserializer) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /serde_combinators/src/mutex.rs: -------------------------------------------------------------------------------- 1 | use super::{DeserializeWith, SerializeWith, compose_deserialize, compose_serialize}; 2 | use serde::{Deserializer, Serializer}; 3 | use std::{marker::PhantomData, sync::Mutex}; 4 | 5 | pub struct MutexF(PhantomData); 6 | 7 | impl SerializeWith for MutexF 8 | where 9 | W: SerializeWith, 10 | { 11 | type T = Mutex; 12 | 13 | fn serialize(value: &Self::T, serializer: S) -> Result 14 | where 15 | S: Serializer, 16 | { 17 | compose_serialize( 18 | |value, serializer, serialize| { 19 | let value = Mutex::try_lock(value).map_err(serde::ser::Error::custom)?; 20 | serialize(&value, serializer) 21 | }, 22 | W::serialize, 23 | )(value, serializer) 24 | } 25 | } 26 | 27 | impl DeserializeWith for MutexF 28 | where 29 | W: DeserializeWith, 30 | { 31 | type T = Mutex; 32 | 33 | fn deserialize<'de, D>(deserializer: D) -> Result 34 | where 35 | D: Deserializer<'de>, 36 | { 37 | compose_deserialize(W::deserialize, |value| Ok(Mutex::new(value)))(deserializer) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /serde_combinators/src/rw_lock.rs: -------------------------------------------------------------------------------- 1 | use super::{DeserializeWith, SerializeWith, compose_deserialize, compose_serialize}; 2 | use serde::{Deserializer, Serializer}; 3 | use std::{marker::PhantomData, sync::RwLock}; 4 | 5 | pub struct RwLockF(PhantomData); 6 | 7 | impl SerializeWith for RwLockF 8 | where 9 | W: SerializeWith, 10 | { 11 | type T = RwLock; 12 | 13 | fn serialize(value: &Self::T, serializer: S) -> Result 14 | where 15 | S: Serializer, 16 | { 17 | compose_serialize( 18 | |value, serializer, serialize| { 19 | let value = RwLock::try_read(value).map_err(serde::ser::Error::custom)?; 20 | serialize(&value, serializer) 21 | }, 22 | W::serialize, 23 | )(value, serializer) 24 | } 25 | } 26 | 27 | impl DeserializeWith for RwLockF 28 | where 29 | W: DeserializeWith, 30 | { 31 | type T = RwLock; 32 | 33 | fn deserialize<'de, D>(deserializer: D) -> Result 34 | where 35 | D: Deserializer<'de>, 36 | { 37 | compose_deserialize(W::deserialize, |value| Ok(RwLock::new(value)))(deserializer) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/serde_format.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::corpus_directory_from_target; 2 | use std::{ 3 | fs::{File, read_dir, remove_dir_all}, 4 | io::Read, 5 | }; 6 | use testing::{CommandExt, examples}; 7 | 8 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 9 | #[test] 10 | fn serde_format() { 11 | let corpus = corpus_directory_from_target("serde", "unit_variant::target"); 12 | 13 | remove_dir_all(&corpus).unwrap_or_default(); 14 | 15 | examples::test("serde", "unit_variant::test") 16 | .unwrap() 17 | .logged_assert() 18 | .success(); 19 | 20 | for entry in read_dir(corpus).unwrap() { 21 | let entry = entry.unwrap(); 22 | let path = entry.path(); 23 | let mut file = File::open(path).unwrap(); 24 | let mut buf = Vec::new(); 25 | file.read_to_end(&mut buf).unwrap(); 26 | // smoelius: CBOR stores the variant name. Hence, this test will fail if CBOR is used as the 27 | // serialization format. 28 | // smoelius: CBOR is no longer a supported format. Still, I see no reason to remove this 29 | // test or the next check. 30 | assert!(!buf.iter().any(u8::is_ascii_uppercase)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dylint.toml: -------------------------------------------------------------------------------- 1 | [workspace.metadata.dylint] 2 | libraries = [ 3 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/general" }, 4 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/supplementary" }, 5 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/assert_eq_arg_misordering" }, 6 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/collapsible_unwrap" }, 7 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/const_path_join" }, 8 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/inconsistent_qualification" }, 9 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/misleading_variable_name" }, 10 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/question_mark_in_expression" }, 11 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/ref_aware_redundant_closure_for_method_calls" }, 12 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/suboptimal_pattern" }, 13 | { git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/try_io_result" }, 14 | ] 15 | -------------------------------------------------------------------------------- /examples/tests/from.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(dylint_lib = "general", allow(crate_wide_allow))] 2 | #![allow(clippy::disallowed_names)] 3 | #![allow(clippy::from_over_into)] 4 | #![allow(clippy::needless_arbitrary_self_type)] 5 | #![allow(clippy::similar_names)] 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Clone, Deserialize, Serialize)] 10 | struct Foo; 11 | 12 | #[derive(Clone, Deserialize, Serialize)] 13 | struct Bar(Option); 14 | 15 | #[derive(Clone, Deserialize, Serialize)] 16 | struct Baz(Foo); 17 | 18 | #[test_fuzz::test_fuzz_impl] 19 | impl From for Bar { 20 | #[test_fuzz::test_fuzz] 21 | fn from(foo: Foo) -> Self { 22 | Self(Some(foo)) 23 | } 24 | } 25 | 26 | #[test_fuzz::test_fuzz_impl] 27 | impl TryFrom for Baz { 28 | type Error = &'static str; 29 | #[test_fuzz::test_fuzz] 30 | fn try_from(bar: Bar) -> Result { 31 | bar.0.ok_or("None").map(Baz) 32 | } 33 | } 34 | 35 | #[test_fuzz::test_fuzz_impl] 36 | impl Into for Baz { 37 | #[test_fuzz::test_fuzz] 38 | fn into(self: Self) -> Foo { 39 | self.0 40 | } 41 | } 42 | 43 | #[test] 44 | fn test() { 45 | let bar: Bar = Foo.into(); 46 | let baz: Baz = bar.try_into().unwrap(); 47 | let _: Foo = baz.into(); 48 | } 49 | -------------------------------------------------------------------------------- /runtime/src/traits.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Add, Div, Sub}; 2 | use num_traits::{Bounded, One}; 3 | 4 | pub trait MinValueAddOne { 5 | fn min_value_add_one() -> Self; 6 | } 7 | 8 | impl MinValueAddOne for T 9 | where 10 | T: Add + Bounded + One, 11 | { 12 | fn min_value_add_one() -> Self { 13 | Self::min_value() + Self::one() 14 | } 15 | } 16 | 17 | pub trait MaxValueSubOne { 18 | fn max_value_sub_one() -> Self; 19 | } 20 | 21 | impl MaxValueSubOne for T 22 | where 23 | T: Sub + Bounded + One, 24 | { 25 | fn max_value_sub_one() -> Self { 26 | Self::max_value() - Self::one() 27 | } 28 | } 29 | 30 | pub trait Two { 31 | fn two() -> Self; 32 | } 33 | 34 | impl Two for T 35 | where 36 | T: Add + One, 37 | { 38 | fn two() -> Self { 39 | Self::one() + Self::one() 40 | } 41 | } 42 | 43 | pub trait Middle { 44 | fn low() -> Self; 45 | fn high() -> Self; 46 | } 47 | 48 | impl Middle for T 49 | where 50 | T: Add + Div + Bounded + One + Two, 51 | { 52 | fn low() -> Self { 53 | Self::min_value() / Self::two() + Self::max_value() / Self::two() 54 | } 55 | fn high() -> Self { 56 | Self::low() + Self::one() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/display.rs: -------------------------------------------------------------------------------- 1 | use predicates::prelude::*; 2 | use testing::{CommandExt, examples}; 3 | 4 | #[test] 5 | fn display_qwerty() { 6 | display("qwerty", "test", "target", "Args { data: \"asdfgh\" }", ""); 7 | } 8 | 9 | #[test] 10 | fn display_debug_crash() { 11 | display( 12 | "debug", 13 | "crash::target_fuzz__::auto_generate", 14 | "crash::target", 15 | "", 16 | "Encountered a failure while not replaying. A buggy Debug implementation perhaps?", 17 | ); 18 | } 19 | 20 | #[test] 21 | fn display_debug_hang() { 22 | display( 23 | "debug", 24 | "hang::target_fuzz__::auto_generate", 25 | "hang::target", 26 | "", 27 | "Encountered a timeout while not replaying. A buggy Debug implementation perhaps?", 28 | ); 29 | } 30 | 31 | fn display(krate: &str, test: &str, target: &str, stdout: &str, stderr: &str) { 32 | examples::test(krate, test) 33 | .unwrap() 34 | .logged_assert() 35 | .success(); 36 | 37 | examples::test_fuzz(krate, target) 38 | .unwrap() 39 | .args(["--display=corpus"]) 40 | .logged_assert() 41 | .success() 42 | .stdout(predicate::str::contains(stdout)) 43 | .stderr(predicate::str::contains(stderr)); 44 | } 45 | -------------------------------------------------------------------------------- /test-fuzz/src/convert.rs: -------------------------------------------------------------------------------- 1 | pub trait FromRef { 2 | fn from_ref(_: &T) -> Self; 3 | } 4 | 5 | impl FromRef for U 6 | where 7 | T: Clone, 8 | U: From, 9 | { 10 | fn from_ref(value: &T) -> Self { 11 | Self::from(value.clone()) 12 | } 13 | } 14 | 15 | /// Trait whose implementation is required by the [`test_fuzz` macro]'s [`convert`] option. 16 | /// 17 | /// The reason for using a non-standard trait is to avoid conflicts that could arise from blanket 18 | /// implementations of standard traits. For example, trying to use `From` instead of 19 | /// `test_fuzz::Into` can lead to errors like the following: 20 | /// 21 | /// ```text 22 | /// conflicting implementation in crate `core`: 23 | /// - impl From for T; 24 | /// ``` 25 | /// 26 | /// Such errors were observed in the [Substrate Node Template] [third-party test]. 27 | /// 28 | /// [`convert`]: https://github.com/trailofbits/test-fuzz/blob/master/README.md#convert--x-y 29 | /// [`test_fuzz` macro]: https://github.com/trailofbits/test-fuzz/blob/master/README.md#test_fuzz-macro 30 | /// [Substrate Node Template]: https://github.com/trailofbits/test-fuzz/blob/master/third-party/patches/substrate_node_template.patch 31 | /// [third-party test]: https://github.com/trailofbits/test-fuzz/blob/master/third-party/tests/third_party.rs 32 | pub trait Into { 33 | fn into(self) -> T; 34 | } 35 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/fuzz_cast.rs: -------------------------------------------------------------------------------- 1 | use predicates::prelude::*; 2 | use testing::{CommandExt, examples, retry}; 3 | 4 | const MAX_TOTAL_TIME: &str = "60"; 5 | 6 | #[test] 7 | fn fuzz_cast() { 8 | examples::test("cast", "test") 9 | .unwrap() 10 | .logged_assert() 11 | .success(); 12 | 13 | for use_cast_checks in [false, true] { 14 | let mut args = vec![ 15 | "--exit-code", 16 | "--run-until-crash", 17 | "--max-total-time", 18 | MAX_TOTAL_TIME, 19 | ]; 20 | let code = if use_cast_checks { 21 | args.push("--features=test-fuzz/cast_checks"); 22 | 1 23 | } else { 24 | 0 25 | }; 26 | retry(3, || { 27 | examples::test_fuzz("cast", "target") 28 | .unwrap() 29 | .args(&args) 30 | .logged_assert() 31 | .try_code(predicate::eq(code)) 32 | }) 33 | .unwrap(); 34 | if use_cast_checks { 35 | examples::test_fuzz("cast", "target") 36 | .unwrap() 37 | .args(["--replay=crashes", "--features=test-fuzz/cast_checks"]) 38 | .logged_assert() 39 | .success() 40 | .stdout(predicate::str::contains("invalid cast")); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/fuzz_profile.rs: -------------------------------------------------------------------------------- 1 | use predicates::prelude::*; 2 | use testing::{CommandExt, examples, retry}; 3 | 4 | const MAX_TOTAL_TIME: &str = "60"; 5 | 6 | #[test] 7 | fn fuzz_profile() { 8 | examples::test("profile", "test") 9 | .unwrap() 10 | .logged_assert() 11 | .success(); 12 | 13 | for use_release in [false, true] { 14 | let mut args = vec![ 15 | "--exit-code", 16 | "--run-until-crash", 17 | "--max-total-time", 18 | MAX_TOTAL_TIME, 19 | ]; 20 | let code = if use_release { 21 | args.push("--release"); 22 | 1 23 | } else { 24 | 0 25 | }; 26 | retry(3, || { 27 | examples::test_fuzz("profile", "target") 28 | .unwrap() 29 | .args(&args) 30 | .logged_assert() 31 | .try_code(predicate::eq(code)) 32 | }) 33 | .unwrap(); 34 | if use_release { 35 | examples::test_fuzz("profile", "target") 36 | .unwrap() 37 | .args(["--replay=crashes", "--release"]) 38 | .logged_assert() 39 | .success() 40 | .stdout(predicate::str::contains( 41 | r#"assertion failed: !x || PROFILE != "release""#, 42 | )); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /third-party/third_party.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "flags": ["SKIP_NIGHTLY"], 4 | "url": "https://github.com/CosmWasm/cw-plus", 5 | "rev": "26e4e1a0e6ab6d57056a3235bdbc9932b87f2abf", 6 | "patch": "cw-plus.patch", 7 | "subdir": ".", 8 | "test_package": "cw20-base", 9 | "fuzz_package": "cw20-base", 10 | "targets": ["instantiate", "execute", "query", "migrate"] 11 | }, 12 | { 13 | "flags": ["EXPENSIVE", "SKIP_NIGHTLY"], 14 | "url": "https://github.com/paritytech/polkadot-sdk", 15 | "rev": "92b7dd505d60377b03cdb9045fb835d33d22c5b5", 16 | "patch": "substrate_client_transaction_pool.patch", 17 | "subdir": ".", 18 | "test_package": "sc-transaction-pool", 19 | "fuzz_package": "sc-transaction-pool", 20 | "targets": ["import"] 21 | }, 22 | { 23 | "flags": [], 24 | "url": "https://github.com/solana-labs/example-helloworld", 25 | "rev": "354e890721068fa5d38e16f380bf994e1c678eb7", 26 | "patch": "example-helloworld.patch", 27 | "subdir": "src/program-rust", 28 | "test_package": "solana-bpf-helloworld", 29 | "fuzz_package": "solana-bpf-helloworld", 30 | "targets": ["process_instruction"] 31 | }, 32 | { 33 | "flags": [], 34 | "url": "https://github.com/anza-xyz/sbpf", 35 | "rev": "3bcd21dc63dd15f6eae49b31fa18a2bc25431bd0", 36 | "patch": "solana-sbpf.patch", 37 | "subdir": ".", 38 | "test_package": "solana-sbpf", 39 | "fuzz_package": "test_utils", 40 | "targets": ["syscall_str_cmp_rust"] 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/fuzz.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::corpus_directory_from_target; 2 | use predicates::prelude::*; 3 | use std::fs::remove_dir_all; 4 | use testing::{CommandExt, examples, retry}; 5 | 6 | const MAX_TOTAL_TIME: &str = "60"; 7 | 8 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 9 | #[test] 10 | fn fuzz_assert() { 11 | fuzz("assert", false); 12 | } 13 | 14 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 15 | #[test] 16 | fn fuzz_qwerty() { 17 | fuzz("qwerty", true); 18 | } 19 | 20 | fn fuzz(krate: &str, persistent: bool) { 21 | let corpus = corpus_directory_from_target(krate, "target"); 22 | 23 | // smoelius: `corpus` is distinct for all tests. So there is no race here. 24 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 25 | remove_dir_all(corpus).unwrap_or_default(); 26 | 27 | examples::test(krate, "test") 28 | .unwrap() 29 | .logged_assert() 30 | .success(); 31 | 32 | retry(3, || { 33 | let mut command = examples::test_fuzz(krate, "target").unwrap(); 34 | 35 | let mut args = vec!["--exit-code", "--run-until-crash"]; 36 | if persistent { 37 | args.push("--persistent"); 38 | } 39 | args.extend_from_slice(&["--max-total-time", MAX_TOTAL_TIME]); 40 | 41 | command 42 | .args(args) 43 | .logged_assert() 44 | .try_code(predicate::eq(1)) 45 | }) 46 | .unwrap(); 47 | } 48 | -------------------------------------------------------------------------------- /examples/tests/associated_type.rs: -------------------------------------------------------------------------------- 1 | // smoelius: Associated types are considered a legitimate reason to put a bound on a struct 2 | // parameter. See: 3 | // * https://github.com/rust-lang/rust-clippy/issues/1689 4 | // * https://github.com/rust-lang/api-guidelines/issues/6 5 | // 6 | // This example is based in part on: 7 | // https://docs.serde.rs/serde_json/#creating-json-by-serializing-data-structures 8 | 9 | use serde::{Deserialize, Serialize, de::DeserializeOwned}; 10 | 11 | trait Serializable { 12 | type Out: Clone + DeserializeOwned + Serialize + PartialEq + Eq; 13 | fn serialize(&self) -> Self::Out; 14 | } 15 | 16 | impl Serializable for T 17 | where 18 | T: Serialize, 19 | { 20 | type Out = String; 21 | fn serialize(&self) -> Self::Out { 22 | serde_json::to_string(self).unwrap() 23 | } 24 | } 25 | 26 | #[test_fuzz::test_fuzz(generic_args = "Address", bounds = "T: Serializable")] 27 | fn serializes_to(x: &T, y: &T::Out) -> bool 28 | where 29 | T: Clone + DeserializeOwned + Serialize + Serializable, 30 | { 31 | &::serialize(x) == y 32 | } 33 | 34 | #[derive(Clone, Serialize, Deserialize)] 35 | struct Address { 36 | street: String, 37 | city: String, 38 | } 39 | 40 | #[test] 41 | fn test() { 42 | let address = Address { 43 | street: "10 Downing Street".to_owned(), 44 | city: "London".to_owned(), 45 | }; 46 | 47 | assert!(serializes_to( 48 | &address, 49 | &String::from("{\"street\":\"10 Downing Street\",\"city\":\"London\"}") 50 | )); 51 | } 52 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/fuzz_generic.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::corpus_directory_from_target; 2 | use predicates::prelude::*; 3 | use std::fs::remove_dir_all; 4 | use testing::{CommandExt, examples, retry}; 5 | 6 | const MAX_TOTAL_TIME: &str = "60"; 7 | 8 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 9 | #[test] 10 | fn fuzz_foo_qwerty() { 11 | // smoelius: When `bincode` is enabled, `cargo-afl` fails because "the program crashed with one 12 | // of the test cases provided." 13 | fuzz("test_foo_qwerty", 2); 14 | } 15 | 16 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 17 | #[test] 18 | fn fuzz_bar_asdfgh() { 19 | fuzz("test_bar_asdfgh", 0); 20 | } 21 | 22 | fn fuzz(test: &str, code: i32) { 23 | let corpus = corpus_directory_from_target("generic", "struct_target"); 24 | 25 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 26 | remove_dir_all(&corpus).unwrap_or_default(); 27 | 28 | examples::test("generic", test) 29 | .unwrap() 30 | .logged_assert() 31 | .success(); 32 | 33 | assert!(corpus.exists()); 34 | 35 | retry(3, || { 36 | examples::test_fuzz("generic", "struct_target") 37 | .unwrap() 38 | .args([ 39 | "--exit-code", 40 | "--run-until-crash", 41 | "--max-total-time", 42 | MAX_TOTAL_TIME, 43 | ]) 44 | .logged_assert() 45 | .try_code(predicate::eq(code)) 46 | }) 47 | .unwrap(); 48 | } 49 | -------------------------------------------------------------------------------- /test-fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-fuzz" 3 | version = "7.2.5" 4 | edition = "2024" 5 | 6 | description = "To make fuzzing Rust easy" 7 | 8 | authors = ["Samuel E. Moelius III "] 9 | license = "AGPL-3.0 WITH mif-exception" 10 | repository = "https://github.com/trailofbits/test-fuzz" 11 | 12 | [dependencies] 13 | afl = { workspace = true, optional = true } 14 | cast_checks = { workspace = true, optional = true } 15 | serde = { workspace = true } 16 | 17 | internal = { workspace = true } 18 | runtime = { workspace = true } 19 | serde_combinators = { workspace = true } 20 | test-fuzz-macro = { workspace = true } 21 | 22 | [dev-dependencies] 23 | assert_cmd = { workspace = true } 24 | cargo_metadata = { workspace = true } 25 | ctor = { workspace = true } 26 | predicates = { workspace = true } 27 | regex = { workspace = true } 28 | semver = { workspace = true } 29 | serde_json = { workspace = true } 30 | similar-asserts = { workspace = true } 31 | tempfile = { workspace = true } 32 | toml_edit = { workspace = true } 33 | walkdir = { workspace = true } 34 | 35 | testing = { workspace = true } 36 | 37 | # smoelius: A list of formats we might support can be found here: 38 | # https://github.com/djkoloski/rust_serialization_benchmark 39 | 40 | [features] 41 | cast_checks = ["dep:cast_checks", "test-fuzz-macro/__cast_checks"] 42 | serde_bincode = ["internal/__serde_bincode"] 43 | serde_postcard = ["internal/__serde_postcard"] 44 | __persistent = ["afl", "test-fuzz-macro/__persistent"] 45 | 46 | [lints] 47 | workspace = true 48 | 49 | [package.metadata.cargo-udeps.ignore] 50 | normal = ["afl"] 51 | -------------------------------------------------------------------------------- /.github/workflows/update_patches.yml: -------------------------------------------------------------------------------- 1 | name: Update patches 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | update: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v6 15 | with: 16 | # https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs 17 | # https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#push-using-ssh-deploy-keys 18 | ssh-key: ${{ secrets.SSH_KEY }} 19 | 20 | - name: Dylint versions 21 | run: cargo search dylint | grep '^dylint' | sort | tee dylint_versions.txt 22 | 23 | # smoelius: The next use of `actions/cache` should match what is in ci.yml. 24 | - uses: actions/cache@v5 25 | with: 26 | path: | 27 | ~/.cargo/bin/ 28 | ~/.cargo/registry/index/ 29 | ~/.cargo/registry/cache/ 30 | ~/.cargo/git/db/ 31 | ~/.dylint_drivers/ 32 | ~/.local/share/afl.rs/ 33 | ~/.rustup/toolchains/ 34 | target/dylint/ 35 | key: stable-dylint-${{ hashFiles('dylint_versions.txt') }} 36 | 37 | - name: Update patches 38 | run: scripts/update_patches.sh 39 | 40 | - name: Create pull request 41 | uses: peter-evans/create-pull-request@v8 42 | with: 43 | title: Update patches 44 | branch: update-patches 45 | branch-suffix: random 46 | commit-message: Update patches 47 | token: ${{ secrets.REPO_TOKEN }} 48 | -------------------------------------------------------------------------------- /cargo-test-fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-test-fuzz" 3 | version = "7.2.5" 4 | edition = "2024" 5 | 6 | description = "cargo-test-fuzz" 7 | 8 | authors = ["Samuel E. Moelius III "] 9 | license = "AGPL-3.0 WITH mif-exception" 10 | repository = "https://github.com/trailofbits/test-fuzz" 11 | default-run = "cargo-test-fuzz" 12 | 13 | [[bin]] 14 | name = "cargo-test-fuzz" 15 | path = "src/bin/cargo_test_fuzz/main.rs" 16 | 17 | [lib] 18 | doctest = false 19 | 20 | [[test]] 21 | name = "install" 22 | required-features = ["test-install"] 23 | 24 | [dependencies] 25 | anyhow = { workspace = true } 26 | bitflags = { workspace = true } 27 | cargo_metadata = { workspace = true } 28 | clap = { workspace = true } 29 | env_logger = { workspace = true } 30 | heck = { workspace = true } 31 | itertools = { workspace = true } 32 | log = { workspace = true } 33 | mio = { workspace = true } 34 | num_cpus = { workspace = true } 35 | remain = { workspace = true } 36 | semver = { workspace = true } 37 | serde = { workspace = true } 38 | strip-ansi-escapes = { workspace = true } 39 | strum_macros = { workspace = true } 40 | subprocess = { workspace = true } 41 | termsize = { workspace = true } 42 | 43 | internal = { workspace = true } 44 | runtime = { workspace = true } 45 | 46 | [dev-dependencies] 47 | assert_cmd = { workspace = true } 48 | predicates = { workspace = true } 49 | rlimit = { workspace = true } 50 | tempfile = { workspace = true } 51 | walkdir = { workspace = true } 52 | xshell = { workspace = true } 53 | 54 | testing = { workspace = true } 55 | 56 | [features] 57 | test-install = [] 58 | 59 | [lints] 60 | workspace = true 61 | 62 | # smoelius: `xshell` is used only by the `install` tests, which are guarded by the `test-install` 63 | # feature. 64 | [package.metadata.cargo-udeps.ignore] 65 | development = ["xshell"] 66 | -------------------------------------------------------------------------------- /serde_combinators/tests/ref_rw_lock.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_assert::{Deserializer, Serializer, Token}; 3 | use serde_combinators::{RefF, RwLockF, Type, With}; 4 | use std::sync::RwLock; 5 | 6 | #[derive(Deserialize, Serialize, Debug)] 7 | struct Struct<'a> { 8 | #[serde(with = "RefF::>>")] 9 | ref_rw_lock: &'a RwLock, 10 | } 11 | 12 | impl PartialEq for Struct<'_> { 13 | fn eq(&self, other: &Self) -> bool { 14 | let x = self.ref_rw_lock.read().unwrap(); 15 | let y = other.ref_rw_lock.read().unwrap(); 16 | *x == *y 17 | } 18 | } 19 | 20 | #[test] 21 | fn eq() { 22 | let rw_lock_a = RwLock::new(0); 23 | let rw_lock_b = RwLock::new(0); 24 | let rw_lock_c = RwLock::new(1); 25 | let strukt_a = Struct { 26 | ref_rw_lock: &rw_lock_a, 27 | }; 28 | let strukt_b = Struct { 29 | ref_rw_lock: &rw_lock_b, 30 | }; 31 | let strukt_c = Struct { 32 | ref_rw_lock: &rw_lock_c, 33 | }; 34 | assert_eq!(strukt_a, strukt_b); 35 | assert_ne!(strukt_a, strukt_c); 36 | } 37 | 38 | #[cfg_attr( 39 | dylint_lib = "assert_eq_arg_misordering", 40 | allow(assert_eq_arg_misordering) 41 | )] 42 | #[test] 43 | fn serde() { 44 | let rw_lock = RwLock::new(0); 45 | let strukt = Struct { 46 | ref_rw_lock: &rw_lock, 47 | }; 48 | let serializer = Serializer::builder().build(); 49 | let tokens = strukt.serialize(&serializer).unwrap(); 50 | assert_eq!( 51 | tokens, 52 | [ 53 | Token::Struct { 54 | name: "Struct", 55 | len: 1 56 | }, 57 | Token::Field("ref_rw_lock"), 58 | Token::I32(0), 59 | Token::StructEnd 60 | ] 61 | ); 62 | let mut deserializer = Deserializer::builder(tokens).build(); 63 | let other = Struct::deserialize(&mut deserializer).unwrap(); 64 | assert_eq!(strukt, other); 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@v6 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Check CHANGELOG.md 19 | run: ./scripts/check_CHANGELOG.sh "${{ github.ref }}" 20 | 21 | - name: Install llvm 22 | run: sudo apt-get install llvm 23 | 24 | - uses: rust-lang/crates-io-auth-action@v1 25 | id: auth 26 | 27 | - name: Publish 28 | run: | 29 | # smoelius: The crates must be published in this order, which is a reverse topological 30 | # sort of `docs/crates.dot`. 31 | for X in internal macro runtime test-fuzz cargo-test-fuzz; do 32 | # smoelius: Continue if a previous publish attempt failed. 33 | TMP="$(mktemp)" 34 | cargo publish --manifest-path "$X"/Cargo.toml 2>"$TMP" || ( 35 | cat "$TMP" | 36 | tee /dev/stderr | 37 | tail -n 1 | 38 | grep '^.*: crate [^`]* already exists on crates.io index$' 39 | ) 40 | done 41 | env: 42 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 43 | 44 | - name: Get version 45 | id: get-version 46 | run: echo "version=${GITHUB_REF/refs\/tags\/v/}" >> "$GITHUB_OUTPUT" 47 | 48 | - name: Create release notes 49 | run: git log -p -1 CHANGELOG.md | grep '^+\($\|[^+]\)' | cut -c 2- | tee body.md 50 | 51 | - name: Create release 52 | uses: softprops/action-gh-release@v2 53 | with: 54 | tag_name: ${{ github.ref }} 55 | name: Release ${{ steps.get-version.outputs.version }} 56 | body_path: body.md 57 | draft: false 58 | prerelease: ${{ contains(github.ref, 'pre') || contains(github.ref, 'rc') }} 59 | token: ${{ secrets.REPO_TOKEN }} 60 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/versions.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::{Dependency, DependencyKind, Metadata, MetadataCommand}; 2 | use semver::Version; 3 | use std::sync::LazyLock; 4 | 5 | static METADATA: LazyLock = 6 | LazyLock::new(|| MetadataCommand::new().no_deps().exec().unwrap()); 7 | 8 | #[test] 9 | fn versions_are_equal() { 10 | for package in &METADATA.packages { 11 | if package.name == "serde_combinators" { 12 | continue; 13 | } 14 | assert_eq!( 15 | env!("CARGO_PKG_VERSION"), 16 | package.version.to_string(), 17 | "{}", 18 | package.name 19 | ); 20 | } 21 | } 22 | 23 | #[test] 24 | fn versions_are_exact_and_match() { 25 | for package in &METADATA.packages { 26 | for Dependency { 27 | name: dep, 28 | req, 29 | kind, 30 | .. 31 | } in &package.dependencies 32 | { 33 | if dep.starts_with("test-fuzz") && kind != &DependencyKind::Development { 34 | assert!( 35 | req.to_string().starts_with('='), 36 | "`{}` dependency on `{}` is not exact", 37 | package.name, 38 | dep 39 | ); 40 | assert!( 41 | req.matches(&Version::parse(env!("CARGO_PKG_VERSION")).unwrap()), 42 | "`{}` dependency on `{}` does not match `{}`", 43 | package.name, 44 | dep, 45 | env!("CARGO_PKG_VERSION"), 46 | ); 47 | } 48 | } 49 | } 50 | } 51 | 52 | // #[test] 53 | #[allow(dead_code)] 54 | fn afl_version_is_exact() { 55 | for package in &METADATA.packages { 56 | for Dependency { name: dep, req, .. } in &package.dependencies { 57 | if dep == "afl" { 58 | assert!( 59 | req.to_string().starts_with('='), 60 | "`{}` dependency on `{}` is not exact", 61 | package.name, 62 | dep 63 | ); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/auto_generate.rs: -------------------------------------------------------------------------------- 1 | use anyhow::ensure; 2 | use internal::dirs::corpus_directory_from_target; 3 | use predicates::prelude::*; 4 | use std::fs::{read_dir, remove_dir_all}; 5 | use testing::{CommandExt, examples, retry}; 6 | 7 | const MAX_TOTAL_TIME: &str = "60"; 8 | 9 | // smoelius: It would be nice if these first two tests could distinguish how many "auto_generate" 10 | // tests get run (0 vs. 1). But right now, I can't think of an easy way to do this. 11 | 12 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 13 | #[test] 14 | fn no_auto_generate() { 15 | auto_generate( 16 | "alloc", 17 | "target", 18 | false, 19 | "Could not find or auto-generate", 20 | 0, 21 | ); 22 | } 23 | 24 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 25 | #[test] 26 | fn auto_generate_empty() { 27 | auto_generate("default", "no_default::target", false, "", 0); 28 | } 29 | 30 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 31 | #[test] 32 | fn auto_generate_nonempty() { 33 | auto_generate("assert", "target", true, "Auto-generated", 1); 34 | } 35 | 36 | fn auto_generate(krate: &str, target: &str, success: bool, pattern: &str, n: usize) { 37 | let corpus = corpus_directory_from_target(krate, target); 38 | 39 | // smoelius: `corpus` is distinct for all tests. So there is no race here. 40 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 41 | remove_dir_all(&corpus).unwrap_or_default(); 42 | 43 | let _: &str = retry(3, || { 44 | let assert = examples::test_fuzz(krate, target) 45 | .unwrap() 46 | .args([ 47 | "--no-ui", 48 | "--run-until-crash", 49 | "--max-total-time", 50 | MAX_TOTAL_TIME, 51 | ]) 52 | .logged_assert(); 53 | 54 | let assert = if success { 55 | assert.success() 56 | } else { 57 | assert.failure() 58 | }; 59 | 60 | assert.try_stderr(predicate::str::contains(pattern))?; 61 | 62 | ensure!(read_dir(&corpus).map(Iterator::count).unwrap_or_default() == n); 63 | 64 | Ok("") 65 | }) 66 | .unwrap(); 67 | } 68 | -------------------------------------------------------------------------------- /serde_combinators/tests/ref_mut_mutex.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_assert::{Deserializer, Serializer, Token}; 3 | use serde_combinators::{MutexF, RefMutF, Type, With}; 4 | use std::sync::Mutex; 5 | 6 | #[derive(Deserialize, Serialize, Debug)] 7 | struct Struct<'a> { 8 | #[serde(with = "RefMutF::>>")] 9 | ref_mut_mutex: &'a mut Mutex, 10 | } 11 | 12 | impl Struct<'_> { 13 | fn swap(&mut self, other: &mut Self) { 14 | let x = self.ref_mut_mutex.get_mut().unwrap(); 15 | let y = other.ref_mut_mutex.get_mut().unwrap(); 16 | std::mem::swap::(x, y); 17 | } 18 | } 19 | 20 | #[allow(clippy::mut_mutex_lock)] 21 | impl PartialEq for Struct<'_> { 22 | fn eq(&self, other: &Self) -> bool { 23 | let x = self.ref_mut_mutex.lock().unwrap(); 24 | let y = other.ref_mut_mutex.lock().unwrap(); 25 | *x == *y 26 | } 27 | } 28 | 29 | #[allow(clippy::mutex_integer)] 30 | #[test] 31 | fn swap() { 32 | let mut mutex_a = Mutex::new(0); 33 | let mut mutex_b = Mutex::new(1); 34 | let mut strukt_a = Struct { 35 | ref_mut_mutex: &mut mutex_a, 36 | }; 37 | let mut strukt_b = Struct { 38 | ref_mut_mutex: &mut mutex_b, 39 | }; 40 | strukt_a.swap(&mut strukt_b); 41 | assert_eq!(1, *mutex_a.lock().unwrap()); 42 | assert_eq!(0, *mutex_b.lock().unwrap()); 43 | } 44 | 45 | #[cfg_attr( 46 | dylint_lib = "assert_eq_arg_misordering", 47 | allow(assert_eq_arg_misordering) 48 | )] 49 | #[allow(clippy::mutex_integer)] 50 | #[test] 51 | fn serde() { 52 | let mut mutex = Mutex::new(0); 53 | let strukt = Struct { 54 | ref_mut_mutex: &mut mutex, 55 | }; 56 | let serializer = Serializer::builder().build(); 57 | let tokens = strukt.serialize(&serializer).unwrap(); 58 | assert_eq!( 59 | tokens, 60 | [ 61 | Token::Struct { 62 | name: "Struct", 63 | len: 1 64 | }, 65 | Token::Field("ref_mut_mutex"), 66 | Token::I32(0), 67 | Token::StructEnd 68 | ] 69 | ); 70 | let mut deserializer = Deserializer::builder(tokens).build(); 71 | let other = Struct::deserialize(&mut deserializer).unwrap(); 72 | assert_eq!(strukt, other); 73 | } 74 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/consolidate.rs: -------------------------------------------------------------------------------- 1 | use anyhow::ensure; 2 | use internal::dirs::corpus_directory_from_target; 3 | use predicates::prelude::*; 4 | use std::fs::{read_dir, remove_dir_all}; 5 | use testing::{CommandExt, examples, retry}; 6 | 7 | const CRASH_MAX_TOTAL_TIME: &str = "60"; 8 | 9 | const HANG_MAX_TOTAL_TIME: &str = "120"; 10 | 11 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 12 | #[test] 13 | fn consolidate_crashes() { 14 | consolidate( 15 | "assert", 16 | "target", 17 | &[ 18 | "--run-until-crash", 19 | "--max-total-time", 20 | CRASH_MAX_TOTAL_TIME, 21 | ], 22 | "Args { x: true }", 23 | ); 24 | } 25 | 26 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 27 | #[test] 28 | fn consolidate_hangs() { 29 | consolidate( 30 | "parse_duration", 31 | "parse", 32 | &["--persistent", "--max-total-time", HANG_MAX_TOTAL_TIME], 33 | "", 34 | ); 35 | } 36 | 37 | fn consolidate(krate: &str, target: &str, fuzz_args: &[&str], pattern: &str) { 38 | let corpus = corpus_directory_from_target(krate, target); 39 | 40 | // smoelius: `corpus` is distinct for all tests. So there is no race here. 41 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 42 | remove_dir_all(&corpus).unwrap_or_default(); 43 | 44 | examples::test(krate, "test") 45 | .unwrap() 46 | .logged_assert() 47 | .success(); 48 | 49 | assert_eq!(1, read_dir(&corpus).unwrap().count()); 50 | 51 | retry(3, || { 52 | let mut args = vec!["--no-ui"]; 53 | args.extend_from_slice(fuzz_args); 54 | 55 | examples::test_fuzz(krate, target) 56 | .unwrap() 57 | .args(args) 58 | .logged_assert() 59 | .success(); 60 | 61 | examples::test_fuzz(krate, target) 62 | .unwrap() 63 | .args(["--consolidate"]) 64 | .logged_assert() 65 | .success(); 66 | 67 | ensure!(read_dir(&corpus).unwrap().count() > 1); 68 | 69 | examples::test_fuzz(krate, target) 70 | .unwrap() 71 | .args(["--display=corpus"]) 72 | .logged_assert() 73 | .success() 74 | .try_stdout(predicate::str::contains(pattern)) 75 | .map_err(Into::into) 76 | }) 77 | .unwrap(); 78 | } 79 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/in_production.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::{corpus_directory_from_target, path_segment}; 2 | use std::{ 3 | env, 4 | ffi::{OsStr, OsString}, 5 | fs::{read_dir, remove_dir_all}, 6 | path::{Component, PathBuf}, 7 | process::Command, 8 | sync::Mutex, 9 | }; 10 | use testing::CommandExt; 11 | 12 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 13 | #[test] 14 | fn no_write() { 15 | test(false, 0); 16 | } 17 | 18 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 19 | #[test] 20 | fn write() { 21 | test(true, 1); 22 | } 23 | 24 | static MUTEX: Mutex<()> = Mutex::new(()); 25 | 26 | fn test(write: bool, n: usize) { 27 | let _lock = MUTEX.lock().unwrap(); 28 | 29 | let thread_specific_corpus = corpus_directory_from_target("hello-world", "target"); 30 | 31 | // smoelius: HACK. `hello-world` writes to `target/corpus`, not, e.g., 32 | // `target/corpus_ThreadId_3_`. For now, just replace `corpus_ThreadId_3_` with `corpus`. 33 | let corpus = thread_specific_corpus 34 | .components() 35 | .map(|component| { 36 | if component.as_os_str() == OsString::from(path_segment("corpus")) { 37 | Component::Normal(OsStr::new("corpus")) 38 | } else { 39 | component 40 | } 41 | }) 42 | .collect::(); 43 | 44 | // smoelius: This call to `remove_dir_all` is protected by the mutex above. 45 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 46 | remove_dir_all(&corpus).unwrap_or_default(); 47 | 48 | let mut command = Command::new("cargo"); 49 | #[cfg_attr(dylint_lib = "general", allow(abs_home_path))] 50 | command.args([ 51 | "run", 52 | "--manifest-path", 53 | concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/Cargo.toml"), 54 | "--features", 55 | &("test-fuzz/".to_owned() + test_fuzz::serde_format::as_feature()), 56 | ]); 57 | 58 | #[cfg_attr(dylint_lib = "general", allow(abs_home_path))] 59 | let mut envs = vec![("TEST_FUZZ_MANIFEST_PATH", env!("CARGO_MANIFEST_PATH"))]; 60 | 61 | if write { 62 | envs.push(("TEST_FUZZ_WRITE", "1")); 63 | } 64 | 65 | command 66 | .current_dir("/tmp") 67 | .envs(envs) 68 | .logged_assert() 69 | .success(); 70 | 71 | assert_eq!(read_dir(corpus).map(Iterator::count).unwrap_or_default(), n); 72 | } 73 | -------------------------------------------------------------------------------- /internal/src/serde_format.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, de::DeserializeOwned}; 2 | use std::io::Read; 3 | 4 | #[cfg(any(serde_default, feature = "__serde_bincode"))] 5 | const BYTE_LIMIT: usize = 1024 * 1024 * 1024; 6 | 7 | // smoelius: I can't find any guidance on how to choose this size. 2048 is used in the `loopback` 8 | // test in the `postcard` repository: 9 | // https://github.com/jamesmunns/postcard/blob/03865c2b7d694d000c0457e8cfaf4ff1b128ed81/tests/loopback.rs#L191 10 | #[cfg(feature = "__serde_postcard")] 11 | const SLIDING_BUFFER_SIZE: usize = 2048; 12 | 13 | #[allow(clippy::vec_init_then_push)] 14 | #[must_use] 15 | pub fn as_feature() -> &'static str { 16 | let mut formats = vec![]; 17 | 18 | #[cfg(any(serde_default, feature = "__serde_bincode"))] 19 | formats.push("serde_bincode"); 20 | 21 | #[cfg(feature = "__serde_postcard")] 22 | formats.push("serde_postcard"); 23 | 24 | assert!( 25 | formats.len() <= 1, 26 | "Multiple serde formats selected: {formats:?}" 27 | ); 28 | 29 | formats.pop().expect("No serde format selected") 30 | } 31 | 32 | pub fn serialize(args: &T) -> Vec { 33 | #[cfg(any(serde_default, feature = "__serde_bincode"))] 34 | return { 35 | // smoelius: From 36 | // https://github.com/bincode-org/bincode/blob/c44b5e364e7084cdbabf9f94b63a3c7f32b8fb68/src/lib.rs#L102-L103 : 37 | // /// **Warning:** the default configuration used by [`bincode::serialize`] is not 38 | // /// the same as that used by the `DefaultOptions` struct. ... 39 | // The point is that `bincode::serialize(..)` and `bincode::options().serialize(..)` use 40 | // different encodings, even though the latter uses "default" options. 41 | // smoelius: With the upgrade to Bincode 2.0, the preceding comments may no longer be 42 | // applicable. 43 | let config = bincode::config::standard().with_limit::(); 44 | bincode::serde::encode_to_vec(args, config).unwrap() 45 | }; 46 | 47 | #[cfg(feature = "__serde_postcard")] 48 | return { 49 | let mut data = Vec::new(); 50 | postcard::to_io(args, &mut data).unwrap(); 51 | data 52 | }; 53 | } 54 | 55 | #[allow(unused_mut)] 56 | pub fn deserialize(mut reader: R) -> Option { 57 | #[cfg(any(serde_default, feature = "__serde_bincode"))] 58 | return { 59 | let config = bincode::config::standard().with_limit::(); 60 | bincode::serde::decode_from_std_read(&mut reader, config).ok() 61 | }; 62 | 63 | #[cfg(feature = "__serde_postcard")] 64 | return { 65 | let mut buff = [0; SLIDING_BUFFER_SIZE]; 66 | postcard::from_io((reader, &mut buff)) 67 | .map(|(value, _)| value) 68 | .ok() 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/fuzz_parallel.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::{corpus_directory_from_target, output_directory_from_target}; 2 | use predicates::prelude::*; 3 | use std::{ffi::OsStr, fs::remove_dir_all}; 4 | use testing::{CommandExt, examples, retry}; 5 | 6 | const CPUS: &str = "2"; 7 | const TIME_SLICE: &str = "30"; 8 | 9 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 10 | #[test] 11 | fn fuzz_parallel() { 12 | for i in 0..6 { 13 | let output_dir = output_directory_from_target("parallel", &format!("target_{i}")); 14 | remove_dir_all(output_dir).unwrap_or_default(); 15 | } 16 | 17 | examples::test("parallel", "test") 18 | .unwrap() 19 | .logged_assert() 20 | .success(); 21 | 22 | retry(3, || { 23 | examples::test_fuzz_inexact("parallel", "target") 24 | .unwrap() 25 | .args([ 26 | "--exit-code", 27 | "--run-until-crash", 28 | "--cpus", 29 | CPUS, 30 | "--slice", 31 | TIME_SLICE, 32 | ]) 33 | .logged_assert() 34 | .try_code(predicate::eq(1)) 35 | }) 36 | .unwrap(); 37 | 38 | // smoelius: Verify that all `.cur_input` files were removed. 39 | for i in 0..6 { 40 | let output_dir = output_directory_from_target("parallel", &format!("target_{i}")); 41 | if output_dir.exists() { 42 | assert!( 43 | !walkdir::WalkDir::new(output_dir) 44 | .into_iter() 45 | .any( 46 | |entry| entry.unwrap().path().file_name() == Some(OsStr::new(".cur_input")) 47 | ) 48 | ); 49 | } 50 | } 51 | } 52 | 53 | const MAX_TOTAL_TIME: &str = "10"; 54 | 55 | #[test] 56 | fn no_premature_termination() { 57 | let corpus_calm = corpus_directory_from_target("calm_and_panicky", "calm"); 58 | remove_dir_all(&corpus_calm).unwrap_or_default(); 59 | 60 | let corpus_panicky = corpus_directory_from_target("calm_and_panicky", "panicky"); 61 | remove_dir_all(&corpus_panicky).unwrap_or_default(); 62 | 63 | examples::test("calm_and_panicky", "test") 64 | .unwrap() 65 | .logged_assert() 66 | .success(); 67 | 68 | let assert = retry(3, || { 69 | examples::test_fuzz_inexact("calm_and_panicky", "") 70 | .unwrap() 71 | .args([ 72 | "--exit-code", 73 | "--run-until-crash", 74 | "--cpus", 75 | CPUS, 76 | "--max-total-time", 77 | MAX_TOTAL_TIME, 78 | ]) 79 | .logged_assert() 80 | .try_code(predicate::eq(0)) 81 | }) 82 | .unwrap(); 83 | 84 | assert.stderr(predicate::str::contains( 85 | "Warning: Command failed for target panicky", 86 | )); 87 | } 88 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/generic_args.rs: -------------------------------------------------------------------------------- 1 | use internal::dirs::{generic_args_directory_from_target, impl_generic_args_directory_from_target}; 2 | use std::fs::remove_dir_all; 3 | use testing::{CommandExt, examples}; 4 | 5 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 6 | #[test] 7 | fn generic() { 8 | let impl_expected = ["generic::Bar", "generic::Foo"]; 9 | let expected = ["generic::Baz", "generic::Baz"]; 10 | test( 11 | "generic", 12 | "test_bound", 13 | "struct_target_bound", 14 | &impl_expected, 15 | &expected, 16 | ); 17 | test( 18 | "generic", 19 | "test_where_clause", 20 | "struct_target_where_clause", 21 | &impl_expected, 22 | &expected, 23 | ); 24 | test( 25 | "generic", 26 | "test_only_generic_args", 27 | "struct_target_only_generic_args", 28 | &impl_expected, 29 | &expected, 30 | ); 31 | } 32 | 33 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 34 | #[test] 35 | fn unserde() { 36 | let impl_expected = [""]; 37 | let expected = ["unserde::Struct"]; 38 | test("unserde", "test", "target", &impl_expected, &expected); 39 | test( 40 | "unserde", 41 | "test_in_production", 42 | "target_in_production", 43 | &impl_expected, 44 | &expected, 45 | ); 46 | } 47 | 48 | fn test(krate: &str, test: &str, target: &str, impl_expected: &[&str], expected: &[&str]) { 49 | let impl_generic_args = impl_generic_args_directory_from_target(krate, target); 50 | 51 | // smoelius: `corpus` is distinct for all tests. So there is no race here. 52 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 53 | remove_dir_all(impl_generic_args).unwrap_or_default(); 54 | 55 | let generic_args = generic_args_directory_from_target(krate, target); 56 | 57 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 58 | remove_dir_all(generic_args).unwrap_or_default(); 59 | 60 | examples::test(krate, test) 61 | .unwrap() 62 | .logged_assert() 63 | .success(); 64 | 65 | for (option, expected) in &[ 66 | ("--display=impl-generic-args", impl_expected), 67 | ("--display=generic-args", expected), 68 | ] { 69 | let assert = &examples::test_fuzz(krate, target) 70 | .unwrap() 71 | .args([option]) 72 | .logged_assert() 73 | .success(); 74 | 75 | let mut actual = std::str::from_utf8(&assert.get_output().stdout) 76 | .unwrap() 77 | .lines() 78 | .map(|s| { 79 | let n = s.find(": ").unwrap(); 80 | s[n + 2..].to_owned() 81 | }) 82 | .collect::>(); 83 | 84 | actual.sort(); 85 | 86 | assert_eq!(expected, &actual); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | # smoelius: If a package that depends on `test-fuzz` is added to the workspace, then 3 | # cargo-test-fuzz/tests/install.rs will need to be updated. 4 | members = [ 5 | "cargo-test-fuzz", 6 | "examples", 7 | "internal", 8 | "macro", 9 | "runtime", 10 | "serde_combinators", 11 | "test-fuzz", 12 | "testing", 13 | "third-party", 14 | ] 15 | resolver = "2" 16 | 17 | [workspace.dependencies] 18 | afl = { version = "0.17" } 19 | anyhow = { version = "1.0", features = ["backtrace"] } 20 | assert_cmd = "2.1" 21 | bitflags = "2.10" 22 | # smoelius: `cargo_metadata` 0.20.0 pulls in too many new dependencies. 23 | cargo_metadata = "=0.19" 24 | cast_checks = { version = "0.1" } 25 | clap = { version = "4.5", features = ["cargo", "derive", "wrap_help"] } 26 | ctor = "0.6" 27 | darling = "0.23" 28 | env_logger = "0.11" 29 | heck = "0.5" 30 | hex = "0.4" 31 | itertools = "0.14" 32 | log = "0.4" 33 | mio = { version = "1.1", features = ["os-ext", "os-poll"] } 34 | num_cpus = "1.17" 35 | num-traits = "0.2" 36 | option_set = "0.4" 37 | parse_duration = "2.1" 38 | predicates = "3.1" 39 | prettyplease = "0.2" 40 | proc-macro2 = "1.0" 41 | quote = "1.0" 42 | regex = "1.12" 43 | remain = "0.2" 44 | rlimit = "0.10" 45 | rustc_version = "0.4" 46 | semver = "1.0" 47 | serde_assert = "0.8" 48 | serde_json = "1.0" 49 | serde = { version = "1.0", features = ["derive", "rc"] } 50 | sha1 = "0.10" 51 | similar-asserts = "1.7" 52 | strip-ansi-escapes = "0.2" 53 | strum_macros = "0.27" 54 | subprocess = "0.2" 55 | syn = { version = "2.0", features = ["full", "parsing", "visit", "visit-mut"] } 56 | tempfile = "3.23" 57 | termsize = "0.1" 58 | toml_edit = "0.23" 59 | walkdir = "2.5" 60 | xshell = "0.2" 61 | 62 | # smoelius: Internal packages 63 | internal = { path = "internal", package = "test-fuzz-internal", version = "=7.2.5" } 64 | runtime = { path = "runtime", package = "test-fuzz-runtime", version = "=7.2.5" } 65 | test-fuzz = { path = "test-fuzz", version = "=7.2.5" } 66 | test-fuzz-macro = { path = "macro", version = "=7.2.5" } 67 | testing = { path = "testing", package = "test-fuzz-testing" } 68 | 69 | # `serde_combinators` does not sync with `test-fuzz`'s version. 70 | serde_combinators = { path = "serde_combinators", version = "0.1.0" } 71 | 72 | [workspace.lints.rust.unexpected_cfgs] 73 | level = "deny" 74 | check-cfg = [ 75 | "cfg(dylint_lib, values(any()))", 76 | "cfg(fuzzing)", 77 | "cfg(serde_default)", 78 | ] 79 | 80 | [workspace.lints.clippy] 81 | nursery = { level = "warn", priority = -1 } 82 | pedantic = { level = "warn", priority = -1 } 83 | cognitive-complexity = "allow" 84 | collection-is-never-read = "allow" 85 | manual-unwrap-or-default = "allow" 86 | missing-errors-doc = "allow" 87 | missing-panics-doc = "allow" 88 | multiple-bound-locations = "allow" 89 | option-if-let-else = "allow" 90 | redundant-pub-crate = "allow" 91 | struct-field-names = "allow" 92 | 93 | [workspace.metadata.unmaintained] 94 | ignore = [ 95 | "bincode", 96 | "bincode_derive", 97 | "parse_duration", 98 | # https://github.com/alacritty/vte/pull/122#issuecomment-2579278540 99 | "utf8parse", 100 | "wit-bindgen-rt", 101 | ] 102 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/integration/replay.rs: -------------------------------------------------------------------------------- 1 | // smoelius: `rlimit` does not work on macOS. 2 | #![cfg(not(target_os = "macos"))] 3 | 4 | use internal::dirs::corpus_directory_from_target; 5 | use predicates::prelude::*; 6 | use std::fs::remove_dir_all; 7 | use testing::{CommandExt, examples, retry}; 8 | 9 | // smoelius: The use of `Resource::DATA` affects all threads in the integration test. We should see 10 | // whether there is a more localized way to achieve this goal. 11 | use rlimit::Resource; 12 | 13 | // smoelius: MEMORY_LIMIT must be large enough for the build process to complete. 14 | // smoelius: Bumping `MEMORY_LIMIT` to 8GB per the above comment re `Resource::DATA`. 15 | const MEMORY_LIMIT: u64 = 8 * 1024 * 1024 * 1024; 16 | 17 | const MAX_TOTAL_TIME: &str = "240"; 18 | 19 | #[derive(Clone, Copy)] 20 | enum Object { 21 | Crashes, 22 | Hangs, 23 | } 24 | 25 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 26 | #[test] 27 | fn replay_crashes() { 28 | replay( 29 | "alloc", 30 | "target", 31 | &[ 32 | "--run-until-crash", 33 | "--", 34 | "-m", 35 | &format!("{}", MEMORY_LIMIT / 1024), 36 | ], 37 | Object::Crashes, 38 | r"\bmemory allocation of \d{10,} bytes failed\b|\bcapacity overflow\b", 39 | ); 40 | } 41 | 42 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 43 | #[allow(clippy::trivial_regex)] 44 | #[test] 45 | fn replay_hangs() { 46 | replay( 47 | "parse_duration", 48 | "parse", 49 | &["--persistent", "--max-total-time", MAX_TOTAL_TIME], 50 | Object::Hangs, 51 | r"(?m)\bTimeout$", 52 | ); 53 | } 54 | 55 | fn replay(krate: &str, target: &str, fuzz_args: &[&str], object: Object, re: &str) { 56 | let corpus = corpus_directory_from_target(krate, target); 57 | 58 | // smoelius: `corpus` is distinct for all tests. So there is no race here. 59 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 60 | remove_dir_all(corpus).unwrap_or_default(); 61 | 62 | examples::test(krate, "test") 63 | .unwrap() 64 | .logged_assert() 65 | .success(); 66 | 67 | examples::test_fuzz(krate, target) 68 | .unwrap() 69 | .args(["--reset"]) 70 | .logged_assert() 71 | .success(); 72 | 73 | retry(3, || { 74 | let mut args = vec!["--no-ui"]; 75 | args.extend_from_slice(fuzz_args); 76 | 77 | examples::test_fuzz(krate, target) 78 | .unwrap() 79 | .args(args) 80 | .logged_assert() 81 | .success(); 82 | 83 | // smoelius: The memory limit must be set to replay the crashes, but not the hangs. 84 | Resource::DATA.set(MEMORY_LIMIT, MEMORY_LIMIT).unwrap(); 85 | 86 | let mut command = examples::test_fuzz(krate, target).unwrap(); 87 | 88 | command 89 | .args([match object { 90 | Object::Crashes => "--replay=crashes", 91 | Object::Hangs => "--replay=hangs", 92 | }]) 93 | .logged_assert() 94 | .success() 95 | .try_stdout(predicate::str::is_match(re).unwrap()) 96 | }) 97 | .unwrap(); 98 | } 99 | -------------------------------------------------------------------------------- /serde_combinators/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserializer, Serializer}; 2 | 3 | /// Trait representing a serializing Serde combinator. 4 | /// 5 | /// - [`With::T`] is the type to be serialized. 6 | /// - [`With::serialize`] is what `#[serde(serialize_with = "...")]` expects. 7 | /// 8 | /// See: 9 | pub trait SerializeWith { 10 | type T; 11 | fn serialize(value: &Self::T, serializer: S) -> Result 12 | where 13 | S: Serializer; 14 | } 15 | 16 | /// Trait representing a deserializing Serde combinator. 17 | /// 18 | /// - [`With::T`] is the type to be deserialized. 19 | /// - [`With::deserialize`] is what `#[serde(deserialize_with = "...")]` expects. 20 | /// 21 | /// See: 22 | pub trait DeserializeWith { 23 | type T; 24 | fn deserialize<'de, D>(deserializer: D) -> Result 25 | where 26 | D: Deserializer<'de>; 27 | } 28 | 29 | /// Trait representing a Serde combinator. 30 | /// 31 | /// The fields' roles are the same as in [`SerializeWith`] and [`DeserializeWith`]. 32 | pub trait With { 33 | type T; 34 | fn serialize(value: &Self::T, serializer: S) -> Result 35 | where 36 | S: Serializer; 37 | fn deserialize<'de, D>(deserializer: D) -> Result 38 | where 39 | D: Deserializer<'de>; 40 | } 41 | 42 | impl With for W 43 | where 44 | W: SerializeWith + DeserializeWith::T>, 45 | { 46 | type T = ::T; 47 | fn serialize(value: &Self::T, serializer: S) -> Result 48 | where 49 | S: Serializer, 50 | { 51 | W::serialize(value, serializer) 52 | } 53 | fn deserialize<'de, D>(deserializer: D) -> Result 54 | where 55 | D: Deserializer<'de>, 56 | { 57 | W::deserialize(deserializer) 58 | } 59 | } 60 | 61 | /// Serializes a transformed value using `transform_and_serialize` and `serialize`, passing the 62 | /// latter into the former. 63 | /// 64 | /// Note that the most intuitive signature for this function would have an argument `transform` 65 | /// rather than `transform_and_serialize`. However, some such `transform` functions would need to 66 | /// return a reference to a local variable. An example occurs in [`RwLockF::serialize`]. Its 67 | /// `transform` function would need to return a reference to a [`std::sync::RwLockReadGuard`]. 68 | fn compose_serialize( 69 | transform_and_serialize: impl Fn(&T, S, F) -> Result, 70 | serialize: F, 71 | ) -> impl FnOnce(&T, S) -> Result 72 | where 73 | S: Serializer, 74 | F: FnOnce(&U, S) -> Result, 75 | { 76 | move |value, serializer| transform_and_serialize(value, serializer, serialize) 77 | } 78 | 79 | /// Deserializes using `deserialize` and then transforms the resulting value using `transform`. 80 | fn compose_deserialize<'de, D, T, U>( 81 | deserialize: impl Fn(D) -> Result, 82 | transform: impl Fn(U) -> Result, 83 | ) -> impl Fn(D) -> Result 84 | where 85 | D: Deserializer<'de>, 86 | { 87 | move |deserializer| { 88 | let value = deserialize(deserializer)?; 89 | transform(value) 90 | } 91 | } 92 | 93 | mod mutex; 94 | mod ref_; 95 | mod ref_mut; 96 | mod rw_lock; 97 | mod type_; 98 | 99 | pub use mutex::MutexF; 100 | pub use ref_::RefF; 101 | pub use ref_mut::RefMutF; 102 | pub use rw_lock::RwLockF; 103 | pub use type_::Type; 104 | -------------------------------------------------------------------------------- /examples/tests/generic.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(dylint_lib = "general", allow(crate_wide_allow))] 2 | #![allow(clippy::disallowed_names)] 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use std::{fmt::Debug, sync::LazyLock}; 6 | 7 | trait FooBarTrait {} 8 | 9 | // smoelius: `Foo` is serializable, but not deserializable. 10 | #[derive(Clone, Debug, Serialize)] 11 | enum Foo { 12 | A(String), 13 | } 14 | 15 | impl FooBarTrait for Foo {} 16 | 17 | #[derive(Clone, Debug, Deserialize, Serialize)] 18 | enum Bar { 19 | B(String), 20 | } 21 | 22 | impl FooBarTrait for Bar {} 23 | 24 | trait BazTrait { 25 | fn qux(); 26 | } 27 | 28 | #[derive(Clone, Debug, Deserialize, Serialize)] 29 | struct Baz(T); 30 | 31 | #[test_fuzz::test_fuzz_impl] 32 | impl BazTrait for Baz { 33 | #[test_fuzz::test_fuzz(impl_generic_args = "Bar")] 34 | fn qux() {} 35 | } 36 | 37 | trait Trait { 38 | fn target(&self, x: &T); 39 | fn target_bound(&self, x: &T, y: &U); 40 | fn target_where_clause(&self, x: &T, y: &U) 41 | where 42 | U: BazTrait + Clone + Debug + Serialize; 43 | fn target_only_generic_args(&self, x: &T, y: &U); 44 | } 45 | 46 | #[derive(Clone, Debug, Deserialize, Serialize)] 47 | struct Struct; 48 | 49 | #[test_fuzz::test_fuzz_impl] 50 | impl Trait for Struct { 51 | // smoelius: The Rust docs (https://doc.rust-lang.org/std/fmt/trait.Debug.html#stability) 52 | // state: 53 | // Derived `Debug` formats are not stable, and so may change with future Rust versions. 54 | // So `x` should not be compared to a string constant. 55 | #[allow(clippy::uninlined_format_args)] 56 | #[test_fuzz::test_fuzz(impl_generic_args = "Bar")] 57 | fn target(&self, x: &T) { 58 | assert_ne!( 59 | format!("{:?}", x), 60 | format!("{:?}", Bar::B("qwerty".to_owned())) 61 | ); 62 | } 63 | 64 | #[test_fuzz::test_fuzz(impl_generic_args = "Bar", generic_args = "Baz")] 65 | fn target_bound(&self, x: &T, y: &U) {} 66 | 67 | #[test_fuzz::test_fuzz(impl_generic_args = "Bar", generic_args = "Baz")] 68 | fn target_where_clause(&self, x: &T, y: &U) 69 | where 70 | U: BazTrait + Clone + Debug + Serialize, 71 | { 72 | } 73 | 74 | #[test_fuzz::test_fuzz(only_generic_args)] 75 | fn target_only_generic_args(&self, _: &T, _: &U) {} 76 | } 77 | 78 | static FOO_QWERTY: LazyLock = LazyLock::new(|| Foo::A("qwerty".to_owned())); 79 | static BAR_ASDFGH: LazyLock = LazyLock::new(|| Bar::B("asdfgh".to_owned())); 80 | 81 | #[test] 82 | fn test_foo_qwerty() { 83 | Struct.target(&*FOO_QWERTY); 84 | } 85 | 86 | #[test] 87 | fn test_bar_asdfgh() { 88 | Struct.target(&*BAR_ASDFGH); 89 | } 90 | 91 | #[test] 92 | fn test_bound() { 93 | Struct.target_bound(&*FOO_QWERTY, &Baz(FOO_QWERTY.clone())); 94 | Struct.target_bound(&*BAR_ASDFGH, &Baz(BAR_ASDFGH.clone())); 95 | } 96 | 97 | #[test] 98 | fn test_where_clause() { 99 | Struct.target_where_clause(&*FOO_QWERTY, &Baz(FOO_QWERTY.clone())); 100 | Struct.target_where_clause(&*BAR_ASDFGH, &Baz(BAR_ASDFGH.clone())); 101 | } 102 | 103 | #[test] 104 | fn test_only_generic_args() { 105 | Struct.target_only_generic_args(&*FOO_QWERTY, &Baz(FOO_QWERTY.clone())); 106 | Struct.target_only_generic_args(&*BAR_ASDFGH, &Baz(BAR_ASDFGH.clone())); 107 | } 108 | 109 | mod receiverless_trait_function { 110 | use super::*; 111 | 112 | trait Trait { 113 | fn target(x: &T); 114 | } 115 | 116 | #[test_fuzz::test_fuzz_impl] 117 | impl Trait for Struct { 118 | #[test_fuzz::test_fuzz(impl_generic_args = "Bar")] 119 | fn target(x: &T) {} 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /internal/src/dirs.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::MetadataCommand; 2 | use std::{ 3 | any::type_name, 4 | env, 5 | path::PathBuf, 6 | sync::atomic::{AtomicBool, Ordering}, 7 | }; 8 | 9 | pub static IN_TEST: AtomicBool = AtomicBool::new(false); 10 | 11 | #[must_use] 12 | pub fn impl_generic_args_directory_from_args_type() -> PathBuf { 13 | impl_generic_args_directory().join(path_from_args_type::()) 14 | } 15 | 16 | #[must_use] 17 | pub fn impl_generic_args_directory_from_target(krate: &str, target: &str) -> PathBuf { 18 | impl_generic_args_directory().join(path_from_target(krate, target)) 19 | } 20 | 21 | #[must_use] 22 | pub fn generic_args_directory_from_args_type() -> PathBuf { 23 | generic_args_directory().join(path_from_args_type::()) 24 | } 25 | 26 | #[must_use] 27 | pub fn generic_args_directory_from_target(krate: &str, target: &str) -> PathBuf { 28 | generic_args_directory().join(path_from_target(krate, target)) 29 | } 30 | 31 | #[must_use] 32 | pub fn corpus_directory_from_args_type() -> PathBuf { 33 | corpus_directory().join(path_from_args_type::()) 34 | } 35 | 36 | #[must_use] 37 | pub fn corpus_directory_from_target(krate: &str, target: &str) -> PathBuf { 38 | corpus_directory().join(path_from_target(krate, target)) 39 | } 40 | 41 | #[must_use] 42 | pub fn crashes_directory_from_target(krate: &str, target: &str) -> PathBuf { 43 | output_directory_from_target(krate, target).join("default/crashes") 44 | } 45 | 46 | #[must_use] 47 | pub fn hangs_directory_from_target(krate: &str, target: &str) -> PathBuf { 48 | output_directory_from_target(krate, target).join("default/hangs") 49 | } 50 | 51 | #[must_use] 52 | pub fn queue_directory_from_target(krate: &str, target: &str) -> PathBuf { 53 | output_directory_from_target(krate, target).join("default/queue") 54 | } 55 | 56 | #[must_use] 57 | pub fn output_directory_from_target(krate: &str, target: &str) -> PathBuf { 58 | output_directory().join(path_from_target(krate, target)) 59 | } 60 | 61 | #[must_use] 62 | fn impl_generic_args_directory() -> PathBuf { 63 | target_directory(false).join(path_segment("impl_generic_args")) 64 | } 65 | 66 | #[must_use] 67 | fn generic_args_directory() -> PathBuf { 68 | target_directory(false).join(path_segment("generic_args")) 69 | } 70 | 71 | #[must_use] 72 | fn corpus_directory() -> PathBuf { 73 | target_directory(false).join(path_segment("corpus")) 74 | } 75 | 76 | #[must_use] 77 | fn output_directory() -> PathBuf { 78 | target_directory(true).join(path_segment("output")) 79 | } 80 | 81 | #[must_use] 82 | pub fn path_segment(s: &str) -> String { 83 | let maybe_id = maybe_id(); 84 | format!( 85 | "{s}{}{}", 86 | if maybe_id.is_some() { "_" } else { "" }, 87 | maybe_id.unwrap_or_default() 88 | ) 89 | } 90 | 91 | fn maybe_id() -> Option { 92 | env::var("TEST_FUZZ_ID").ok().or_else(|| { 93 | if IN_TEST.load(Ordering::SeqCst) { 94 | Some(thread_id()) 95 | } else { 96 | None 97 | } 98 | }) 99 | } 100 | 101 | fn thread_id() -> String { 102 | format!("{:?}", std::thread::current().id()).replace(['(', ')'], "_") 103 | } 104 | 105 | #[must_use] 106 | pub fn target_directory(instrumented: bool) -> PathBuf { 107 | let mut command = MetadataCommand::new(); 108 | if let Ok(path) = env::var("TEST_FUZZ_MANIFEST_PATH") { 109 | command.manifest_path(path); 110 | } 111 | let mut target_dir = command.no_deps().exec().unwrap().target_directory; 112 | if instrumented { 113 | target_dir = target_dir.join("afl"); 114 | } 115 | target_dir.into() 116 | } 117 | 118 | #[must_use] 119 | fn path_from_args_type() -> String { 120 | let type_name = type_name::(); 121 | let n = type_name 122 | .find("_fuzz__") 123 | .unwrap_or_else(|| panic!("unexpected type name: `{type_name}`")); 124 | type_name[..n].to_owned() 125 | } 126 | 127 | #[must_use] 128 | fn path_from_target(krate: &str, target: &str) -> String { 129 | krate.replace('-', "_") + "::" + target 130 | } 131 | -------------------------------------------------------------------------------- /test-fuzz/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! **Warning:** The contents of `test_fuzz::utils` are provided for convenience and may be removed 2 | //! in future versions of `test-fuzz`. 3 | 4 | /// Skip values of type `$ty` when serializing. Initialize values of type `$ty` with `$expr` when 5 | /// deserializing. 6 | #[macro_export] 7 | macro_rules! dont_care { 8 | ($ty:path, $expr:expr) => { 9 | impl serde::Serialize for $ty { 10 | fn serialize(&self, serializer: S) -> std::result::Result 11 | where 12 | S: serde::Serializer, 13 | { 14 | ().serialize(serializer) 15 | } 16 | } 17 | 18 | impl<'de> serde::Deserialize<'de> for $ty { 19 | fn deserialize(deserializer: D) -> std::result::Result 20 | where 21 | D: serde::Deserializer<'de>, 22 | { 23 | <()>::deserialize(deserializer).map(|_| $expr) 24 | } 25 | } 26 | }; 27 | ($ty:path) => { 28 | $crate::dont_care!($ty, $ty); 29 | }; 30 | } 31 | 32 | /// Wrap `<$ty as ToOwned>::Owned` in a type `$ident` and implement `From` and `test_fuzz::Into` 33 | /// for `$ident` so that `convert = "&$ty, $ident"` can be used. 34 | #[macro_export] 35 | macro_rules! leak { 36 | ($ty:ty, $ident:ident) => { 37 | #[derive(Clone, std::fmt::Debug, serde::Deserialize, serde::Serialize)] 38 | struct $ident(<$ty as ToOwned>::Owned); 39 | 40 | impl From<&$ty> for $ident { 41 | fn from(ty: &$ty) -> Self { 42 | Self(ty.to_owned()) 43 | } 44 | } 45 | 46 | impl test_fuzz::Into<&$ty> for $ident { 47 | fn into(self) -> &'static $ty { 48 | Box::leak(Box::new(self.0)) 49 | } 50 | } 51 | }; 52 | } 53 | 54 | pub mod serde_ref { 55 | pub use super::deserialize_ref as deserialize; 56 | pub use super::serialize_ref as serialize; 57 | } 58 | 59 | /// `serialize_ref` functions similar to `leak!`, but it is meant to be used with Serde's 60 | /// [`serialize_with`](https://serde.rs/field-attrs.html#serialize_with) field attribute. 61 | #[inline] 62 | pub fn serialize_ref<'a, S, T>(x: &&'a T, serializer: S) -> Result 63 | where 64 | S: serde::Serializer, 65 | T: serde::Serialize, 66 | { 67 | use serde_combinators::{RefF, SerializeWith, Type}; 68 | 69 | > as SerializeWith>::serialize(x, serializer) 70 | } 71 | 72 | /// `deserialize_ref` functions similar to `leak!`, but it is meant to be used with Serde's 73 | /// [`deserialize_with`](https://serde.rs/field-attrs.html#deserialize_with) field attribute. 74 | #[inline] 75 | pub fn deserialize_ref<'de, D, T>(deserializer: D) -> Result<&'static T, D::Error> 76 | where 77 | D: serde::Deserializer<'de>, 78 | T: serde::de::DeserializeOwned + std::fmt::Debug, 79 | { 80 | use serde_combinators::{DeserializeWith, RefF, Type}; 81 | 82 | > as DeserializeWith>::deserialize(deserializer) 83 | } 84 | 85 | pub mod serde_ref_mut { 86 | pub use super::deserialize_ref_mut as deserialize; 87 | pub use super::serialize_ref_mut as serialize; 88 | } 89 | 90 | /// `serialize_ref_mut` is similar to `serialize_ref`, except it operates on a mutable reference 91 | /// instead of an immutable one. 92 | pub fn serialize_ref_mut<'a, S, T>(x: &&'a mut T, serializer: S) -> Result 93 | where 94 | S: serde::Serializer, 95 | T: serde::Serialize, 96 | { 97 | use serde_combinators::{RefMutF, SerializeWith, Type}; 98 | 99 | > as SerializeWith>::serialize(x, serializer) 100 | } 101 | 102 | /// `deserialize_ref_mut` is similar to `deserialize_ref`, except it operates on a mutable reference 103 | /// instead of an immutable one. 104 | pub fn deserialize_ref_mut<'de, D, T>(deserializer: D) -> Result<&'static mut T, D::Error> 105 | where 106 | D: serde::Deserializer<'de>, 107 | T: serde::de::DeserializeOwned + std::fmt::Debug, 108 | { 109 | use serde_combinators::{DeserializeWith, RefMutF, Type}; 110 | 111 | > as DeserializeWith>::deserialize(deserializer) 112 | } 113 | -------------------------------------------------------------------------------- /examples/tests/conversion.rs: -------------------------------------------------------------------------------- 1 | mod path { 2 | use std::path::Path; 3 | use test_fuzz::leak; 4 | 5 | leak!(Path, LeakedPath); 6 | 7 | #[test_fuzz::test_fuzz(convert = "&Path, LeakedPath")] 8 | fn target(path: &Path) {} 9 | 10 | #[test] 11 | fn test() { 12 | target(Path::new("/")); 13 | } 14 | } 15 | 16 | mod array { 17 | use test_fuzz::leak; 18 | 19 | leak!([String], LeakedArray); 20 | 21 | #[test_fuzz::test_fuzz(convert = "&[String], LeakedArray")] 22 | fn target(path: &[String]) {} 23 | 24 | #[test] 25 | fn test() { 26 | target(&[String::from("x")]); 27 | } 28 | } 29 | 30 | mod receiver { 31 | use serde::{Deserialize, Serialize}; 32 | 33 | #[derive(Clone)] 34 | struct X; 35 | 36 | #[derive(Clone, Deserialize, Serialize)] 37 | struct Y; 38 | 39 | impl From<&X> for Y { 40 | fn from(_: &X) -> Self { 41 | Self 42 | } 43 | } 44 | 45 | impl test_fuzz::Into<&'static X> for Y { 46 | fn into(self) -> &'static X { 47 | Box::leak(Box::new(X)) 48 | } 49 | } 50 | 51 | // smoelius: FIXME 52 | #[allow(clippy::use_self)] 53 | #[test_fuzz::test_fuzz_impl] 54 | impl X { 55 | #[test_fuzz::test_fuzz(convert = "&X, Y")] 56 | fn target_self(&self) {} 57 | 58 | #[test_fuzz::test_fuzz(convert = "&X, Y")] 59 | fn target_other(other: &X) {} 60 | } 61 | 62 | #[test] 63 | fn test() { 64 | X.target_self(); 65 | X::target_other(&X); 66 | } 67 | } 68 | 69 | mod lifetime { 70 | use serde::{Deserialize, Serialize}; 71 | 72 | #[derive(Clone)] 73 | struct X<'a>(&'a bool); 74 | 75 | #[derive(Clone, Deserialize, Serialize)] 76 | struct Y(bool); 77 | 78 | impl From> for Y { 79 | fn from(x: X) -> Self { 80 | Self(*x.0) 81 | } 82 | } 83 | 84 | impl test_fuzz::Into> for Y { 85 | fn into(self) -> X<'static> { 86 | X(Box::leak(Box::new(self.0))) 87 | } 88 | } 89 | 90 | #[test_fuzz::test_fuzz(convert = "X<'a>, Y")] 91 | fn target<'a>(x: X<'a>) {} 92 | 93 | #[test] 94 | fn test() { 95 | target(X(&false)); 96 | } 97 | } 98 | 99 | mod mutable { 100 | use serde::{Deserialize, Serialize}; 101 | 102 | #[derive(Clone)] 103 | struct X(bool); 104 | 105 | #[derive(Clone, Deserialize, Serialize)] 106 | struct Y(bool); 107 | 108 | impl From for Y { 109 | fn from(x: X) -> Self { 110 | Self(x.0) 111 | } 112 | } 113 | 114 | impl test_fuzz::Into for Y { 115 | fn into(self) -> X { 116 | X(self.0) 117 | } 118 | } 119 | 120 | #[test_fuzz::test_fuzz(convert = "X, Y")] 121 | fn target(mut x: X) { 122 | x.0 = true; 123 | } 124 | 125 | #[test] 126 | fn test() { 127 | target(X(false)); 128 | } 129 | } 130 | 131 | mod uncloneable { 132 | use serde::{Deserialize, Serialize}; 133 | 134 | struct X; 135 | 136 | #[derive(Clone, Deserialize, Serialize)] 137 | struct Y; 138 | 139 | impl test_fuzz::FromRef for Y { 140 | fn from_ref(_: &X) -> Self { 141 | Self 142 | } 143 | } 144 | 145 | impl test_fuzz::Into for Y { 146 | fn into(self) -> X { 147 | X 148 | } 149 | } 150 | 151 | #[test_fuzz::test_fuzz(convert = "X, Y")] 152 | fn target(x: X) {} 153 | 154 | #[test] 155 | fn test() { 156 | target(X); 157 | } 158 | } 159 | 160 | #[cfg(feature = "__inapplicable_conversion")] 161 | mod inapplicable_conversion { 162 | use serde::{Deserialize, Serialize}; 163 | 164 | #[derive(Clone)] 165 | struct X; 166 | 167 | #[derive(Clone)] 168 | struct Y; 169 | 170 | #[derive(Clone, Deserialize, Serialize)] 171 | struct Z; 172 | 173 | impl From for Z { 174 | fn from(_: Y) -> Self { 175 | Self 176 | } 177 | } 178 | 179 | impl test_fuzz::Into for Z { 180 | fn into(self) -> Y { 181 | Y 182 | } 183 | } 184 | 185 | #[test_fuzz::test_fuzz_impl] 186 | impl X { 187 | #[test_fuzz::test_fuzz(convert = "Y, Z")] 188 | fn target(self) {} 189 | } 190 | 191 | #[test] 192 | fn test() { 193 | X.target(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /third-party/patches/cw-plus.patch: -------------------------------------------------------------------------------- 1 | diff --git a/contracts/cw20-base/Cargo.toml b/contracts/cw20-base/Cargo.toml 2 | index e3cffc3..77ad75e 100644 3 | --- a/contracts/cw20-base/Cargo.toml 4 | +++ b/contracts/cw20-base/Cargo.toml 5 | @@ -28,4 +28,6 @@ serde = { workspace = true } 6 | thiserror = { workspace = true } 7 | 8 | +test-fuzz = { path = "../../../../test-fuzz" } 9 | + 10 | [dev-dependencies] 11 | cw-multi-test = { workspace = true } 12 | diff --git a/contracts/cw20-base/src/contract.rs b/contracts/cw20-base/src/contract.rs 13 | index c74281f..5bc9718 100644 14 | --- a/contracts/cw20-base/src/contract.rs 15 | +++ b/contracts/cw20-base/src/contract.rs 16 | @@ -91,7 +91,33 @@ fn verify_logo(logo: &Logo) -> Result<(), ContractError> { 17 | } 18 | 19 | +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 20 | +struct MockDepsMut; 21 | + 22 | +impl<'a> test_fuzz::FromRef> for MockDepsMut { 23 | + fn from_ref(_: &DepsMut) -> Self { 24 | + Self 25 | + } 26 | +} 27 | + 28 | +impl<'a> test_fuzz::Into> for MockDepsMut { 29 | + fn into(self) -> DepsMut<'a> { 30 | + let cosmwasm_std::OwnedDeps { 31 | + storage, 32 | + api, 33 | + querier, 34 | + .. 35 | + } = cosmwasm_std::testing::mock_dependencies(); 36 | + DepsMut { 37 | + storage: Box::leak(Box::new(storage)), 38 | + api: Box::leak(Box::new(api)), 39 | + querier: cosmwasm_std::QuerierWrapper::new(Box::leak(Box::new(querier))), 40 | + } 41 | + } 42 | +} 43 | + 44 | #[cfg_attr(not(feature = "library"), entry_point)] 45 | -pub fn instantiate( 46 | - mut deps: DepsMut, 47 | +#[test_fuzz::test_fuzz(convert = "DepsMut<'a>, MockDepsMut")] 48 | +pub fn instantiate<'a>( 49 | + mut deps: DepsMut<'a>, 50 | _env: Env, 51 | _info: MessageInfo, 52 | @@ -185,6 +211,7 @@ pub fn validate_accounts(accounts: &[Cw20Coin]) -> Result<(), ContractError> { 53 | 54 | #[cfg_attr(not(feature = "library"), entry_point)] 55 | -pub fn execute( 56 | - deps: DepsMut, 57 | +#[test_fuzz::test_fuzz(convert = "DepsMut<'a>, MockDepsMut")] 58 | +pub fn execute<'a>( 59 | + deps: DepsMut<'a>, 60 | env: Env, 61 | info: MessageInfo, 62 | @@ -504,6 +531,32 @@ pub fn execute_upload_logo( 63 | } 64 | 65 | +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 66 | +struct MockDeps; 67 | + 68 | +impl<'a> From> for MockDeps { 69 | + fn from(_: Deps) -> Self { 70 | + Self 71 | + } 72 | +} 73 | + 74 | +impl<'a> test_fuzz::Into> for MockDeps { 75 | + fn into(self) -> Deps<'a> { 76 | + let cosmwasm_std::OwnedDeps { 77 | + storage, 78 | + api, 79 | + querier, 80 | + .. 81 | + } = cosmwasm_std::testing::mock_dependencies(); 82 | + Deps { 83 | + storage: Box::leak(Box::new(storage)), 84 | + api: Box::leak(Box::new(api)), 85 | + querier: cosmwasm_std::QuerierWrapper::new(Box::leak(Box::new(querier))), 86 | + } 87 | + } 88 | +} 89 | + 90 | #[cfg_attr(not(feature = "library"), entry_point)] 91 | -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { 92 | +#[test_fuzz::test_fuzz(convert = "Deps<'a>, MockDeps")] 93 | +pub fn query<'a>(deps: Deps<'a>, _env: Env, msg: QueryMsg) -> StdResult { 94 | match msg { 95 | QueryMsg::Balance { address } => to_json_binary(&query_balance(deps, address)?), 96 | @@ -587,5 +640,10 @@ pub fn query_download_logo(deps: Deps) -> StdResult { 97 | 98 | #[cfg_attr(not(feature = "library"), entry_point)] 99 | -pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { 100 | +#[test_fuzz::test_fuzz(convert = "DepsMut<'a>, MockDepsMut")] 101 | +pub fn migrate<'a>( 102 | + deps: DepsMut<'a>, 103 | + _env: Env, 104 | + _msg: MigrateMsg, 105 | +) -> Result { 106 | let original_version = 107 | ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; 108 | diff --git a/contracts/cw20-base/src/msg.rs b/contracts/cw20-base/src/msg.rs 109 | index 2088712..bf9e251 100644 110 | --- a/contracts/cw20-base/src/msg.rs 111 | +++ b/contracts/cw20-base/src/msg.rs 112 | @@ -123,5 +123,5 @@ pub enum QueryMsg { 113 | } 114 | 115 | -#[derive(Serialize, Deserialize, JsonSchema)] 116 | +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] 117 | pub struct MigrateMsg {} 118 | 119 | -------------------------------------------------------------------------------- /testing/src/examples.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result, bail, ensure}; 2 | use cargo_metadata::{Artifact, ArtifactProfile, Message}; 3 | use internal::serde_format; 4 | use log::debug; 5 | use std::{path::Path, process::Command, sync::LazyLock}; 6 | use subprocess::{Exec, Redirection}; 7 | 8 | pub static MANIFEST_PATH: LazyLock = LazyLock::new(|| { 9 | #[cfg_attr(dylint_lib = "general", allow(abs_home_path))] 10 | Path::new(env!("CARGO_MANIFEST_DIR")) 11 | .join("../examples/Cargo.toml") 12 | .to_string_lossy() 13 | .to_string() 14 | }); 15 | 16 | // smoelius: We want to reuse the existing features. So we can't do anything that would cause the 17 | // examples to be rebuilt. 18 | // smoelius: What was I worried about? What would cause the examples to be rebuilt? Something about 19 | // the current working directory? Maybe using the manifest path solves that problem? 20 | pub fn test(krate: &str, test: &str) -> Result { 21 | // smoelius: Put --message-format=json last so that it is easy to copy-and-paste the command 22 | // without it. 23 | let serde_format_feature = "test-fuzz/".to_owned() + serde_format::as_feature(); 24 | let mut args = vec![ 25 | "test", 26 | "--manifest-path", 27 | &*MANIFEST_PATH, 28 | "--test", 29 | krate, 30 | "--features", 31 | &serde_format_feature, 32 | ]; 33 | args.extend_from_slice(&["--no-run", "--message-format=json"]); 34 | 35 | let exec = Exec::cmd("cargo").args(&args).stdout(Redirection::Pipe); 36 | debug!("{exec:?}"); 37 | let mut popen = exec.clone().popen()?; 38 | let messages = popen 39 | .stdout 40 | .take() 41 | .map_or(Ok(vec![]), |stream| -> Result<_> { 42 | let reader = std::io::BufReader::new(stream); 43 | let messages: Vec = Message::parse_stream(reader) 44 | .collect::>() 45 | .with_context(|| format!("`parse_stream` failed for `{exec:?}`"))?; 46 | Ok(messages) 47 | })?; 48 | let status = popen 49 | .wait() 50 | .with_context(|| format!("`wait` failed for `{popen:?}`"))?; 51 | 52 | ensure!(status.success(), "Command failed: {exec:?}"); 53 | 54 | let executables = messages 55 | .into_iter() 56 | .filter_map(|message| { 57 | if let Message::CompilerArtifact(Artifact { 58 | profile: ArtifactProfile { test: true, .. }, 59 | executable: Some(executable), 60 | .. 61 | }) = message 62 | && let Some(file_name) = executable.file_name() 63 | && file_name.starts_with(&(krate.to_owned() + "-")) 64 | { 65 | Some(executable) 66 | } else { 67 | None 68 | } 69 | }) 70 | .collect::>(); 71 | 72 | ensure!( 73 | executables.len() <= 1, 74 | "Found multiple executables starting with `{krate}`" 75 | ); 76 | 77 | if let Some(executable) = executables.into_iter().next() { 78 | let mut command = Command::new(executable); 79 | command.env("TEST_FUZZ_ID", id()); 80 | command.args(["--exact", test]); 81 | Ok(command) 82 | } else { 83 | bail!("Found no executables starting with `{krate}`") 84 | } 85 | } 86 | 87 | pub fn test_fuzz_all() -> Result { 88 | let serde_format_feature = "test-fuzz/".to_owned() + serde_format::as_feature(); 89 | #[cfg_attr(dylint_lib = "general", allow(abs_home_path))] 90 | let args = vec![ 91 | "run", 92 | "--bin=cargo-test-fuzz", 93 | "--manifest-path", 94 | concat!(env!("CARGO_MANIFEST_DIR"), "/../Cargo.toml"), 95 | "--", 96 | "test-fuzz", 97 | "--manifest-path", 98 | &*MANIFEST_PATH, 99 | "--features", 100 | &serde_format_feature, 101 | ]; 102 | 103 | let mut command = Command::new("cargo"); 104 | command.env("AFL_NO_AFFINITY", "1"); 105 | command.env("TEST_FUZZ_ID", id()); 106 | command.args(args); 107 | Ok(command) 108 | } 109 | 110 | pub fn test_fuzz(krate: &str, target: &str) -> Result { 111 | test_fuzz_all().map(|mut command| { 112 | command.args(["--test", krate, "--exact", target]); 113 | command 114 | }) 115 | } 116 | 117 | pub fn test_fuzz_inexact(krate: &str, target: &str) -> Result { 118 | test_fuzz_all().map(|mut command| { 119 | command.args(["--test", krate, target]); 120 | command 121 | }) 122 | } 123 | 124 | fn id() -> String { 125 | std::env::var("TEST_FUZZ_ID").unwrap_or_else(|_| thread_id()) 126 | } 127 | 128 | fn thread_id() -> String { 129 | format!("{:?}", std::thread::current().id()).replace(['(', ')'], "_") 130 | } 131 | -------------------------------------------------------------------------------- /third-party/patches/example-helloworld.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/program-rust/Cargo.toml b/src/program-rust/Cargo.toml 2 | index a4976a0..90124ef 100644 3 | --- a/src/program-rust/Cargo.toml 4 | +++ b/src/program-rust/Cargo.toml 5 | @@ -18,4 +18,9 @@ borsh-derive = "0.10.0" 6 | solana-program = "~1.10.35" 7 | 8 | +serde = "1.0" 9 | + 10 | +[target.'cfg(not(target_arch = "bpf"))'.dependencies] 11 | +test-fuzz = { path = "../../../../test-fuzz" } 12 | + 13 | [dev-dependencies] 14 | solana-program-test = "~1.10.35" 15 | @@ -25,2 +30,4 @@ solana-sdk = "~1.10.35" 16 | name = "helloworld" 17 | crate-type = ["cdylib", "lib"] 18 | + 19 | +[workspace] 20 | diff --git a/src/program-rust/src/lib.rs b/src/program-rust/src/lib.rs 21 | index 88714a8..026b733 100644 22 | --- a/src/program-rust/src/lib.rs 23 | +++ b/src/program-rust/src/lib.rs 24 | @@ -19,8 +19,108 @@ pub struct GreetingAccount { 25 | entrypoint!(process_instruction); 26 | 27 | +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 28 | +struct AccountInfoElem { 29 | + key: Pubkey, 30 | + is_signer: bool, 31 | + is_writable: bool, 32 | + lamports: u64, 33 | + data: Vec, 34 | + owner: Pubkey, 35 | + executable: bool, 36 | + rent_epoch: solana_program::clock::Epoch, 37 | +} 38 | + 39 | +impl<'a> From> for AccountInfoElem { 40 | + fn from(account_info: AccountInfo<'a>) -> Self { 41 | + let AccountInfo { 42 | + key, 43 | + is_signer, 44 | + is_writable, 45 | + lamports, 46 | + data, 47 | + owner, 48 | + executable, 49 | + rent_epoch, 50 | + } = account_info; 51 | + let key = *key; 52 | + let lamports = **lamports.borrow(); 53 | + let data = (*data.borrow()).to_vec(); 54 | + let owner = *owner; 55 | + Self { 56 | + key, 57 | + is_signer, 58 | + is_writable, 59 | + lamports, 60 | + data, 61 | + owner, 62 | + executable, 63 | + rent_epoch, 64 | + } 65 | + } 66 | +} 67 | + 68 | +#[cfg(not(target_arch = "bpf"))] 69 | +impl<'a> test_fuzz::Into> for AccountInfoElem { 70 | + fn into(self) -> AccountInfo<'a> { 71 | + let Self { 72 | + key, 73 | + is_signer, 74 | + is_writable, 75 | + lamports, 76 | + data, 77 | + owner, 78 | + executable, 79 | + rent_epoch, 80 | + } = self; 81 | + AccountInfo { 82 | + key: Box::leak(Box::new(key)), 83 | + is_signer, 84 | + is_writable, 85 | + lamports: std::rc::Rc::new(std::cell::RefCell::new(Box::leak(Box::new(lamports)))), 86 | + data: std::rc::Rc::new(std::cell::RefCell::new( 87 | + Box::leak(Box::new(data)).as_mut_slice(), 88 | + )), 89 | + owner: Box::leak(Box::new(owner)), 90 | + executable, 91 | + rent_epoch, 92 | + } 93 | + } 94 | +} 95 | + 96 | +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 97 | +struct AccountInfoVec(Vec); 98 | + 99 | +impl<'a> From<&[AccountInfo<'a>]> for AccountInfoVec { 100 | + fn from(account_infos: &[AccountInfo<'a>]) -> Self { 101 | + Self( 102 | + account_infos 103 | + .iter() 104 | + .map(|account_info| AccountInfoElem::from(account_info.clone())) 105 | + .collect(), 106 | + ) 107 | + } 108 | +} 109 | + 110 | +#[cfg(not(target_arch = "bpf"))] 111 | +impl<'a> test_fuzz::Into<&'static [AccountInfo<'a>]> for AccountInfoVec { 112 | + fn into(self) -> &'static [AccountInfo<'a>] { 113 | + Box::leak(Box::new( 114 | + self.0 115 | + .into_iter() 116 | + .map(test_fuzz::Into::into) 117 | + .collect::>(), 118 | + )) 119 | + .as_slice() 120 | + } 121 | +} 122 | + 123 | // Program entrypoint's implementation 124 | -pub fn process_instruction( 125 | +#[cfg_attr( 126 | + not(target_arch = "bpf"), 127 | + test_fuzz::test_fuzz(convert = "&[AccountInfo<'a>], AccountInfoVec") 128 | +)] 129 | +pub fn process_instruction<'a>( 130 | program_id: &Pubkey, // Public key of the account the hello world program was loaded into 131 | - accounts: &[AccountInfo], // The account to say hello to 132 | + accounts: &[AccountInfo<'a>], // The account to say hello to 133 | _instruction_data: &[u8], // Ignored, all helloworld instructions are hellos 134 | ) -> ProgramResult { 135 | @@ -41,5 +141,8 @@ pub fn process_instruction( 136 | // Increment and store the number of times the account has been greeted 137 | let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?; 138 | - greeting_account.counter += 1; 139 | + greeting_account.counter = greeting_account 140 | + .counter 141 | + .checked_add(1) 142 | + .ok_or(ProgramError::InvalidAccountData)?; 143 | greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?; 144 | 145 | -------------------------------------------------------------------------------- /runtime/src/lib.rs: -------------------------------------------------------------------------------- 1 | use internal::{ 2 | dirs::{ 3 | corpus_directory_from_args_type, generic_args_directory_from_args_type, 4 | impl_generic_args_directory_from_args_type, 5 | }, 6 | serde_format, 7 | }; 8 | use serde::{Serialize, de::DeserializeOwned}; 9 | use sha1::{Digest, Sha1}; 10 | use std::{ 11 | any::type_name, 12 | env, 13 | fmt::{self, Debug, Formatter}, 14 | fs::{create_dir_all, write}, 15 | io::{self, Read, Write}, 16 | marker::PhantomData, 17 | path::Path, 18 | sync::Once, 19 | }; 20 | 21 | pub use num_traits; 22 | 23 | pub mod traits; 24 | 25 | // smoelius: TryDebug, etc. use Nikolai Vazquez's trick from `impls`. 26 | // https://github.com/nvzqz/impls#how-it-works 27 | 28 | struct DebugUnimplemented(PhantomData); 29 | 30 | impl Debug for DebugUnimplemented { 31 | // smoelius: The third party-tests check for the absence of "unknown of type..." messages. So if 32 | // the messages change, the third-party tests must be updated. 33 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 34 | const PAT: &str = "TryDebug<"; 35 | let type_name = type_name::(); 36 | let pos = type_name.find(PAT).unwrap() + PAT.len(); 37 | write!( 38 | f, 39 | "", 40 | type_name[pos..].strip_suffix('>').unwrap() 41 | ) 42 | } 43 | } 44 | 45 | pub trait TryDebugFallback { 46 | fn apply(&self, f: &mut dyn FnMut(&dyn Debug) -> U) -> U; 47 | } 48 | 49 | impl TryDebugFallback for T { 50 | fn apply(&self, f: &mut dyn FnMut(&dyn Debug) -> U) -> U { 51 | f(&DebugUnimplemented::(PhantomData)) 52 | } 53 | } 54 | 55 | pub struct TryDebug<'a, T>(pub &'a T); 56 | 57 | impl TryDebug<'_, T> { 58 | pub fn apply(&self, f: &mut dyn FnMut(&dyn Debug) -> U) -> U { 59 | f(self.0) 60 | } 61 | } 62 | 63 | /// * `ty` - Type. When `auto_impl!` is used in the `auto!` macro, this should be just `$ty`. 64 | /// * `trait` - Trait that `ty` may or may not implement. 65 | /// * `expr` - Vector of values using `trait`, should `ty` implement it. In `expr`, `T` should be 66 | /// used as the name of the type, not `$ty`. 67 | #[macro_export] 68 | macro_rules! auto_impl { 69 | ($ty:ty, $trait:path, $expr:expr) => {{ 70 | trait AutoFallback { 71 | fn auto() -> Vec; 72 | } 73 | 74 | impl AutoFallback for T { 75 | #[must_use] 76 | fn auto() -> Vec { 77 | vec![] 78 | } 79 | } 80 | 81 | struct Auto(std::marker::PhantomData); 82 | 83 | impl Auto { 84 | #[must_use] 85 | pub fn auto() -> Vec { 86 | $expr 87 | } 88 | } 89 | 90 | Auto::<$ty>::auto() as Vec<$ty> 91 | }}; 92 | } 93 | 94 | #[macro_export] 95 | macro_rules! auto { 96 | ($ty:ty) => {{ 97 | let xss = [ 98 | $crate::auto_impl!( 99 | $ty, 100 | $crate::num_traits::bounds::Bounded, 101 | vec![T::min_value(), T::max_value()] 102 | ), 103 | $crate::auto_impl!($ty, Default, vec![T::default()]), 104 | $crate::auto_impl!( 105 | $ty, 106 | $crate::traits::MaxValueSubOne, 107 | vec![T::max_value_sub_one()] 108 | ), 109 | $crate::auto_impl!($ty, $crate::traits::Middle, vec![T::low(), T::high()]), 110 | $crate::auto_impl!( 111 | $ty, 112 | $crate::traits::MinValueAddOne, 113 | vec![T::min_value_add_one()] 114 | ), 115 | ]; 116 | IntoIterator::into_iter(xss).flatten() 117 | }}; 118 | } 119 | 120 | pub fn warn_if_test_fuzz_not_enabled() { 121 | static ONCE: Once = Once::new(); 122 | ONCE.call_once(|| { 123 | if !test_fuzz_enabled() { 124 | #[allow(clippy::explicit_write)] 125 | writeln!( 126 | io::stderr(), 127 | "If you are trying to run a test-fuzz-generated fuzzing harness, be sure to run \ 128 | with `TEST_FUZZ=1`." 129 | ) 130 | .unwrap(); 131 | } 132 | }); 133 | } 134 | 135 | #[must_use] 136 | pub fn test_fuzz_enabled() -> bool { 137 | enabled("") 138 | } 139 | 140 | #[must_use] 141 | pub fn display_enabled() -> bool { 142 | enabled("DISPLAY") 143 | } 144 | 145 | #[must_use] 146 | pub fn pretty_print_enabled() -> bool { 147 | enabled("PRETTY_PRINT") 148 | } 149 | 150 | #[must_use] 151 | pub fn replay_enabled() -> bool { 152 | enabled("REPLAY") 153 | } 154 | 155 | #[must_use] 156 | pub fn write_enabled() -> bool { 157 | enabled("WRITE") 158 | } 159 | 160 | #[must_use] 161 | fn enabled(opt: &str) -> bool { 162 | let key = "TEST_FUZZ".to_owned() + if opt.is_empty() { "" } else { "_" } + opt; 163 | env::var(key).is_ok_and(|value| value != "0") 164 | } 165 | 166 | pub fn write_impl_generic_args(args: &[&str]) { 167 | let impl_generic_args = impl_generic_args_directory_from_args_type::(); 168 | let data = args.join(", "); 169 | write_data(&impl_generic_args, data.as_bytes()).unwrap(); 170 | } 171 | 172 | pub fn write_generic_args(args: &[&str]) { 173 | let generic_args = generic_args_directory_from_args_type::(); 174 | let data = args.join(", "); 175 | write_data(&generic_args, data.as_bytes()).unwrap(); 176 | } 177 | 178 | pub fn write_args(args: &T) { 179 | let corpus = corpus_directory_from_args_type::(); 180 | let data = serde_format::serialize(args); 181 | write_data(&corpus, &data).unwrap(); 182 | } 183 | 184 | pub fn write_data(dir: &Path, data: &[u8]) -> io::Result<()> { 185 | create_dir_all(dir).unwrap_or_default(); 186 | let hex = { 187 | let digest = Sha1::digest(data); 188 | hex::encode(digest) 189 | }; 190 | let path_buf = dir.join(hex); 191 | write(path_buf, data) 192 | } 193 | 194 | pub fn read_args(reader: R) -> Option { 195 | serde_format::deserialize(reader) 196 | } 197 | -------------------------------------------------------------------------------- /macro/src/type_utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Punct, Spacing, Span, TokenStream, TokenTree}; 2 | use quote::ToTokens; 3 | use std::collections::BTreeMap; 4 | use syn::{ 5 | GenericArgument, Ident, Path, PathArguments, PathSegment, Type, TypePath, parse_quote, 6 | visit::{Visit, visit_path_arguments}, 7 | visit_mut::{VisitMut, visit_type_mut}, 8 | }; 9 | 10 | pub fn map_path_generic_params(map: &BTreeMap<&Ident, &GenericArgument>, path: &Path) -> Path { 11 | let mut path = path.clone(); 12 | let mut visitor = GenericParamVisitor { map }; 13 | visitor.visit_path_mut(&mut path); 14 | path 15 | } 16 | 17 | pub fn map_type_generic_params(map: &BTreeMap<&Ident, &GenericArgument>, ty: &Type) -> Type { 18 | let mut ty = ty.clone(); 19 | let mut visitor = GenericParamVisitor { map }; 20 | visitor.visit_type_mut(&mut ty); 21 | ty 22 | } 23 | 24 | struct GenericParamVisitor<'a> { 25 | map: &'a BTreeMap<&'a Ident, &'a GenericArgument>, 26 | } 27 | 28 | impl VisitMut for GenericParamVisitor<'_> { 29 | fn visit_type_mut(&mut self, ty: &mut Type) { 30 | if let Type::Path(TypePath { qself: None, path }) = ty 31 | && let Some(ident) = path.get_ident() 32 | && let Some(generic_arg) = self.map.get(ident) 33 | { 34 | let GenericArgument::Type(ty_new) = generic_arg else { 35 | panic!( 36 | "Unexpected generic argument: {}", 37 | generic_arg.to_token_stream() 38 | ); 39 | }; 40 | *ty = ty_new.clone(); 41 | return; 42 | } 43 | visit_type_mut(self, ty); 44 | } 45 | } 46 | 47 | pub fn path_as_turbofish(path: &Path) -> TokenStream { 48 | let tokens = path.to_token_stream().into_iter().collect::>(); 49 | let mut visitor = TurbofishVisitor { tokens }; 50 | visitor.visit_path(path); 51 | visitor.tokens.into_iter().collect() 52 | } 53 | 54 | pub fn type_as_turbofish(ty: &Type) -> TokenStream { 55 | let tokens = ty.to_token_stream().into_iter().collect::>(); 56 | let mut visitor = TurbofishVisitor { tokens }; 57 | visitor.visit_type(ty); 58 | visitor.tokens.into_iter().collect() 59 | } 60 | 61 | struct TurbofishVisitor { 62 | tokens: Vec, 63 | } 64 | 65 | impl Visit<'_> for TurbofishVisitor { 66 | fn visit_path_arguments(&mut self, path_args: &PathArguments) { 67 | if !path_args.is_none() { 68 | let mut visitor_token_strings = token_strings(&self.tokens); 69 | let path_args_tokens = path_args.to_token_stream().into_iter().collect::>(); 70 | let path_args_token_strings = token_strings(&path_args_tokens); 71 | let n = path_args_tokens.len(); 72 | let mut i: usize = 0; 73 | while i + n <= self.tokens.len() { 74 | if visitor_token_strings[i..i + n] == path_args_token_strings 75 | && (i < 2 || visitor_token_strings[i - 2..i] != [":", ":"]) 76 | { 77 | self.tokens = [ 78 | &self.tokens[..i], 79 | &[ 80 | TokenTree::Punct(Punct::new(':', Spacing::Joint)), 81 | TokenTree::Punct(Punct::new(':', Spacing::Alone)), 82 | ], 83 | &self.tokens[i..], 84 | ] 85 | .concat(); 86 | visitor_token_strings = token_strings(&self.tokens); 87 | i += 2; 88 | } 89 | i += 1; 90 | } 91 | } 92 | visit_path_arguments(self, path_args); 93 | } 94 | } 95 | 96 | fn token_strings(tokens: &[TokenTree]) -> Vec { 97 | tokens.iter().map(ToString::to_string).collect::>() 98 | } 99 | 100 | pub fn expand_self(trait_path: Option<&Path>, self_ty: &Type, ty: &Type) -> Type { 101 | let mut ty = ty.clone(); 102 | let mut visitor = ExpandSelfVisitor { 103 | trait_path, 104 | self_ty, 105 | }; 106 | visitor.visit_type_mut(&mut ty); 107 | ty 108 | } 109 | 110 | struct ExpandSelfVisitor<'a> { 111 | trait_path: Option<&'a Path>, 112 | self_ty: &'a Type, 113 | } 114 | 115 | impl VisitMut for ExpandSelfVisitor<'_> { 116 | fn visit_type_mut(&mut self, ty: &mut Type) { 117 | // smoelius: Rewrite this using if-let-guards once the feature is stable. 118 | // https://rust-lang.github.io/rfcs/2294-if-let-guard.html 119 | if let Type::Path(path) = ty { 120 | if match_type_path(path, &["Self"]) == Some(PathArguments::None) { 121 | *ty = self.self_ty.clone(); 122 | return; 123 | } else if path.qself.is_none() 124 | && path 125 | .path 126 | .segments 127 | .first() 128 | .is_some_and(|segment| segment.ident == "Self") 129 | { 130 | let segments = path.path.segments.iter().skip(1).collect::>(); 131 | let self_ty = self.self_ty; 132 | let trait_path = self 133 | .trait_path 134 | .as_ref() 135 | .expect("`trait_path` should be set"); 136 | *ty = parse_quote! { < #self_ty as #trait_path > :: #(#segments)::* }; 137 | return; 138 | } 139 | } 140 | visit_type_mut(self, ty); 141 | } 142 | } 143 | 144 | pub fn match_type_path(path: &TypePath, other: &[&str]) -> Option { 145 | let mut path = path.clone(); 146 | let args = path.path.segments.last_mut().map(|segment| { 147 | let args = segment.arguments.clone(); 148 | segment.arguments = PathArguments::None; 149 | args 150 | }); 151 | let lhs = path.path.segments.into_iter().collect::>(); 152 | let rhs = other 153 | .iter() 154 | .map(|s| { 155 | let ident = Ident::new(s, Span::call_site()); 156 | PathSegment { 157 | ident, 158 | arguments: PathArguments::None, 159 | } 160 | }) 161 | .collect::>(); 162 | if path.qself.is_none() && lhs == rhs { 163 | args 164 | } else { 165 | None 166 | } 167 | } 168 | 169 | pub fn type_base(ty: &Type) -> Option<&Ident> { 170 | if let Type::Path(path) = ty 171 | && let Some(segment) = path.path.segments.last() 172 | { 173 | return Some(&segment.ident); 174 | } 175 | 176 | None 177 | } 178 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | merge_group: 5 | 6 | pull_request: 7 | 8 | # smoelius: Every Thursday at 3:00 UTC (Wednesday at 22:00 EST), run `cargo test -- --ignored`. 9 | schedule: 10 | - cron: "0 3 * * 4" 11 | 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: ci-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | env: 19 | CARGO_TERM_COLOR: always 20 | GROUP_RUNNER: target.'cfg(all())'.runner='group-runner' 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | check-up-to-dateness: 27 | outputs: 28 | is-up-to-date: ${{ steps.main.outputs.is-up-to-date }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - id: main 32 | uses: trailofbits/check-up-to-dateness@v1 33 | 34 | test: 35 | needs: [check-up-to-dateness] 36 | 37 | if: needs.check-up-to-dateness.outputs.is-up-to-date != 'true' 38 | 39 | runs-on: ubuntu-latest 40 | 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | package: [third-party, other] 45 | serde_format: [bincode, postcard] 46 | toolchain: [stable, nightly] 47 | sha1_filenames: [false, true] 48 | 49 | steps: 50 | - uses: actions/checkout@v6 51 | 52 | - name: Dylint versions 53 | run: cargo search dylint | grep '^dylint' | sort | tee dylint_versions.txt 54 | 55 | # smoelius: The `~/.cargo/` entries are from: 56 | # * https://github.com/actions/cache/blob/main/examples.md#rust---cargo. 57 | # * https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci 58 | # The rest were added by me. 59 | - uses: actions/cache@v5 60 | with: 61 | path: | 62 | ~/.cargo/bin/ 63 | ~/.cargo/registry/index/ 64 | ~/.cargo/registry/cache/ 65 | ~/.cargo/git/db/ 66 | ~/.dylint_drivers/ 67 | ~/.local/share/afl.rs/ 68 | ~/.rustup/toolchains/ 69 | target/dylint/ 70 | key: ${{ matrix.toolchain }}-dylint-${{ hashFiles('dylint_versions.txt') }} 71 | 72 | # smoelius: The Substrate tests require the `rust-src` component and the wasm32 target. 73 | - name: Set toolchain 74 | run: | 75 | rustup default ${{ matrix.toolchain }} 76 | rustup component add rust-src 77 | rustup target add wasm32-unknown-unknown 78 | 79 | # smoelius: The Substrate tests require `protoc`. 80 | - name: Install protoc 81 | run: sudo apt-get install protobuf-compiler 82 | 83 | # smoelius: Some of the `install` tests run older versions of cargo-afl that still use the 84 | # gold linker. However, the gold linker does not work with the nightly toolchain. See: 85 | # https://github.com/rust-fuzz/afl.rs/pull/597 86 | - name: Remove gold linker 87 | run: | 88 | sudo rm -f /usr/bin/ld.gold 89 | sudo ln -s /usr/bin/ld /usr/bin/ld.gold 90 | 91 | - name: Install cargo-afl 92 | run: cargo install cargo-afl || true 93 | 94 | - name: Run afl-system-config 95 | run: cargo afl system-config 96 | 97 | - uses: taiki-e/install-action@v2 98 | with: 99 | tool: cargo-udeps 100 | 101 | # smoelius: I expect this list to grow. 102 | - name: Install tools 103 | run: | 104 | rustup +nightly component add clippy rustfmt 105 | cargo install cargo-dylint dylint-link || true 106 | cargo install cargo-license || true 107 | cargo install cargo-supply-chain || true 108 | cargo install cargo-unmaintained || true 109 | cargo install group-runner || true 110 | go install github.com/rhysd/actionlint/cmd/actionlint@latest 111 | npm install -g prettier 112 | 113 | - name: Free up space 114 | run: | 115 | # https://github.com/actions/runner-images/issues/2606#issuecomment-772683150 116 | sudo rm -rf /usr/local/lib/android 117 | sudo rm -rf /usr/share/dotnet 118 | sudo rm -rf /usr/share/swift 119 | # du -sh /usr/*/* 2>/dev/null | sort -h || true 120 | 121 | - name: Setup 122 | run: | 123 | if [[ ${{ matrix.package }} = 'third-party' ]]; then 124 | MAYBE_THIRD_PARTY='--package third-party' 125 | if [[ ${{ github.event_name }} = 'schedule' ]] || 126 | git diff --name-only ${{ github.event.pull_request.base.sha }} | grep -w 'patches\|third_party' >/dev/null 127 | then 128 | MAYBE_THIRD_PARTY="$MAYBE_THIRD_PARTY --features=test-third-party-full" 129 | fi 130 | else 131 | MAYBE_THIRD_PARTY='--workspace --exclude third-party --features=test-install' 132 | fi 133 | SERDE_FORMAT='test-fuzz/serde_${{ matrix.serde_format }}' 134 | MAYBE_SHUFFLE= 135 | if [[ ${{ matrix.toolchain }} = nightly ]]; then 136 | MAYBE_SHUFFLE='-Z unstable-options --shuffle --test-threads=1' 137 | fi 138 | BUILD_CMD="cargo build $MAYBE_THIRD_PARTY --features $SERDE_FORMAT --all-targets" 139 | TEST_CMD="cargo test $MAYBE_THIRD_PARTY --features $SERDE_FORMAT --config $GROUP_RUNNER -- --nocapture $MAYBE_SHUFFLE" 140 | echo "BUILD_CMD=$BUILD_CMD" >> "$GITHUB_ENV" 141 | echo "TEST_CMD=$TEST_CMD" >> "$GITHUB_ENV" 142 | if ${{ matrix.sha1_filenames }}; then 143 | echo 'AFL_SHA1_FILENAMES=1' >> "$GITHUB_ENV" 144 | fi 145 | 146 | - name: Build 147 | run: $BUILD_CMD 148 | 149 | - name: Test 150 | run: | 151 | $TEST_CMD 152 | env: 153 | AFL_NO_AFFINITY: 1 154 | RUST_BACKTRACE: 1 155 | RUST_LOG: warn 156 | 157 | - name: Check for non-SHA1 filenames 158 | if: ${{ matrix.sha1_filenames }} 159 | run: | 160 | if find target -name 'id:*' | grep .; then 161 | exit 1 162 | fi 163 | 164 | all-checks: 165 | needs: 166 | - test 167 | 168 | runs-on: ubuntu-latest 169 | 170 | # smoelius: From "Defining prerequisite jobs" 171 | # (https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow#defining-prerequisite-jobs): 172 | # > If you would like a job to run even if a job it is dependent on did not succeed, use the 173 | # > `always()` conditional expression in `jobs..if`. 174 | if: ${{ always() }} 175 | 176 | steps: 177 | - name: Check results 178 | if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} 179 | run: exit 1 180 | -------------------------------------------------------------------------------- /cargo-test-fuzz/src/bin/cargo_test_fuzz/transition.rs: -------------------------------------------------------------------------------- 1 | use super::Object; 2 | use anyhow::Result; 3 | use clap::{ArgAction, Parser, crate_version}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::ffi::OsStr; 6 | 7 | #[derive(Debug, Parser)] 8 | #[command(bin_name = "cargo")] 9 | struct Opts { 10 | #[clap(subcommand)] 11 | subcmd: SubCommand, 12 | } 13 | 14 | #[derive(Debug, Parser)] 15 | enum SubCommand { 16 | TestFuzz(TestFuzzWithDeprecations), 17 | } 18 | 19 | // smoelius: Wherever possible, try to reuse cargo test and libtest option names. 20 | #[allow(clippy::struct_excessive_bools)] 21 | #[derive(Clone, Debug, Deserialize, Parser, Serialize)] 22 | #[command(version = crate_version!(), after_help = "\ 23 | Try `cargo afl fuzz --help` to see additional fuzzer options. 24 | ")] 25 | #[remain::sorted] 26 | struct TestFuzzWithDeprecations { 27 | #[arg(long, help = "Display backtraces")] 28 | backtrace: bool, 29 | #[arg( 30 | long, 31 | help = "Move one target's crashes, hangs, and work queue to its corpus; to consolidate \ 32 | all targets, use --consolidate-all" 33 | )] 34 | consolidate: bool, 35 | #[arg(long, hide = true)] 36 | consolidate_all: bool, 37 | #[arg( 38 | long, 39 | value_name = "N", 40 | help = "Fuzz using at most cpus; default is all but one" 41 | )] 42 | cpus: Option, 43 | #[arg( 44 | long, 45 | value_name = "OBJECT", 46 | hide_possible_values = true, 47 | help = "Display corpus, crashes, generic args, `impl` generic args, hangs, or work queue. \ 48 | By default, an uninstrumented fuzz target is used. To display with \ 49 | instrumentation, append `-instrumented` to , e.g., --display \ 50 | corpus-instrumented." 51 | )] 52 | display: Option, 53 | #[arg(long, help = "Target name is an exact name rather than a substring")] 54 | exact: bool, 55 | #[arg( 56 | long, 57 | help = "Exit with 0 if the time limit was reached, 1 for other programmatic aborts, and 2 \ 58 | if an error occurred; implies --no-ui, does not imply --run-until-crash or \ 59 | --max-total-time " 60 | )] 61 | exit_code: bool, 62 | #[arg( 63 | long, 64 | action = ArgAction::Append, 65 | help = "Space or comma separated list of features to activate" 66 | )] 67 | features: Vec, 68 | #[arg(long, help = "List fuzz targets")] 69 | list: bool, 70 | #[arg(long, value_name = "PATH", help = "Path to Cargo.toml")] 71 | manifest_path: Option, 72 | #[arg( 73 | long, 74 | value_name = "SECONDS", 75 | help = "Fuzz at most of time (equivalent to -- -V )" 76 | )] 77 | max_total_time: Option, 78 | #[arg(long, help = "Do not activate the `default` feature")] 79 | no_default_features: bool, 80 | #[arg(long, hide = true)] 81 | no_instrumentation: bool, 82 | #[arg(long, help = "Compile, but don't fuzz")] 83 | no_run: bool, 84 | #[arg(long, help = "Disable user interface")] 85 | no_ui: bool, 86 | #[arg(short, long, help = "Package containing fuzz target")] 87 | package: Option, 88 | #[arg(long, help = "Enable persistent mode fuzzing")] 89 | persistent: bool, 90 | #[arg( 91 | long, 92 | help = "Pretty-print debug output when displaying/replaying", 93 | alias = "pretty-print" 94 | )] 95 | pretty: bool, 96 | #[arg(long, help = "Build in release mode")] 97 | release: bool, 98 | #[arg( 99 | long, 100 | value_name = "OBJECT", 101 | hide_possible_values = true, 102 | help = "Replay corpus, crashes, hangs, or work queue. By default, an uninstrumented fuzz \ 103 | target is used. To replay with instrumentation append `-instrumented` to \ 104 | , e.g., --replay corpus-instrumented." 105 | )] 106 | replay: Option, 107 | #[arg( 108 | long, 109 | help = "Clear fuzzing data for one target, but leave corpus intact; to reset all targets, \ 110 | use --reset-all" 111 | )] 112 | reset: bool, 113 | #[arg(long, hide = true)] 114 | reset_all: bool, 115 | #[arg(long, help = "Resume target's last fuzzing session")] 116 | resume: bool, 117 | #[arg(long, help = "Stop fuzzing once a crash is found")] 118 | run_until_crash: bool, 119 | #[arg( 120 | long, 121 | value_name = "SECONDS", 122 | default_value = "1200", 123 | help = "If there are not sufficiently many cpus to fuzz all targets simultaneously, fuzz \ 124 | them in intervals of " 125 | )] 126 | slice: u64, 127 | #[arg( 128 | long, 129 | value_name = "NAME", 130 | help = "Integration test containing fuzz target" 131 | )] 132 | test: Option, 133 | #[arg( 134 | long, 135 | help = "Number of seconds to consider a hang when fuzzing or replaying (equivalent to -- \ 136 | -t when fuzzing)" 137 | )] 138 | timeout: Option, 139 | #[arg(long, help = "Show build output when displaying/replaying")] 140 | verbose: bool, 141 | #[arg( 142 | value_name = "TARGETNAME", 143 | help = "String that fuzz target's name must contain" 144 | )] 145 | ztarget: Option, 146 | #[arg(last = true, name = "ARGS", help = "Arguments for the fuzzer")] 147 | zzargs: Vec, 148 | } 149 | 150 | impl From for super::TestFuzz { 151 | fn from(opts: TestFuzzWithDeprecations) -> Self { 152 | let TestFuzzWithDeprecations { 153 | backtrace, 154 | consolidate, 155 | consolidate_all, 156 | cpus, 157 | display, 158 | exact, 159 | exit_code, 160 | features, 161 | list, 162 | manifest_path, 163 | max_total_time, 164 | no_default_features, 165 | no_instrumentation, 166 | no_run, 167 | no_ui, 168 | package, 169 | persistent, 170 | pretty, 171 | release, 172 | replay, 173 | reset, 174 | reset_all, 175 | resume, 176 | run_until_crash, 177 | slice, 178 | test, 179 | timeout, 180 | verbose, 181 | ztarget, 182 | zzargs, 183 | } = opts; 184 | if no_instrumentation { 185 | eprintln!( 186 | "--no-instrumentation is now the default. This option will be removed in a future \ 187 | version of test-fuzz.", 188 | ); 189 | } 190 | Self { 191 | backtrace, 192 | consolidate, 193 | consolidate_all, 194 | cpus, 195 | display, 196 | exact, 197 | exit_code, 198 | features, 199 | list, 200 | manifest_path, 201 | max_total_time, 202 | no_default_features, 203 | no_run, 204 | no_ui, 205 | package, 206 | persistent, 207 | pretty, 208 | release, 209 | replay, 210 | reset, 211 | reset_all, 212 | resume, 213 | run_until_crash, 214 | slice, 215 | test, 216 | timeout, 217 | verbose, 218 | ztarget, 219 | zzargs, 220 | } 221 | } 222 | } 223 | 224 | #[allow(unused_macros)] 225 | macro_rules! process_deprecated_action_object { 226 | ($opts:ident, $action:ident, $object_old:ident, $object_new:ident) => { 227 | if $opts.$action == Some(Object::$object_old) { 228 | use heck::ToKebabCase; 229 | eprintln!( 230 | "{}` is deprecated. Use `{}`.", 231 | stringify!($object_old).to_kebab_case(), 232 | stringify!($object_new).to_kebab_case(), 233 | ); 234 | $opts.$action = Some(Object::$object_new); 235 | } 236 | }; 237 | } 238 | 239 | pub(crate) fn cargo_test_fuzz>(args: &[T]) -> Result<()> { 240 | let SubCommand::TestFuzz(opts) = Opts::parse_from(args).subcmd; 241 | 242 | super::run(super::TestFuzz::from(opts)) 243 | } 244 | 245 | #[test] 246 | fn verify_cli() { 247 | use clap::CommandFactory; 248 | Opts::command().debug_assert(); 249 | } 250 | -------------------------------------------------------------------------------- /cargo-test-fuzz/tests/install.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(dylint_lib = "general", allow(crate_wide_allow))] 2 | // smoelius: All work is done in temporary directories. So there are no races. 3 | #![cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 4 | #![cfg_attr(dylint_lib = "try_io_result", allow(try_io_result))] 5 | 6 | use anyhow::Result; 7 | use std::{env::join_paths, fs::write, path::Path}; 8 | use tempfile::NamedTempFile; 9 | use xshell::{Shell, cmd}; 10 | 11 | /* 12 | test-uninstalled-cargo-afl: 13 | ... 14 | - name: Test 15 | run: | 16 | OUTPUT="$(cargo run -p cargo-test-fuzz -- test-fuzz -p test-fuzz-examples --no-run 2>&1 1>/dev/null || true)" 17 | echo "$OUTPUT" 18 | echo "$OUTPUT" | grep '^Error: Could not determine `cargo-afl` version. Is it installed? Try `cargo install afl`.$' 19 | */ 20 | #[test] 21 | fn uninstalled_cargo_afl() -> Result<()> { 22 | run_test( 23 | None, 24 | &[], 25 | &[ 26 | "\\ Result<()> { 46 | run_test( 47 | Some("0.13.2"), 48 | &[], 49 | &[ 50 | "^Error: `[^`]*` depends on `afl [^`]*`, which is incompatible with `cargo-afl \ 51 | [^`]*`.$", 52 | ], 53 | ) 54 | } 55 | 56 | /* 57 | test-newer-afl: 58 | ... 59 | - name: Install afl 0.12.2 60 | run: cargo install afl --version=0.12.2 61 | 62 | - name: Require afl 0.12.3 63 | run: | 64 | sed -i 's/^\(afl = {.*\ Result<()> { 75 | run_test( 76 | Some("0.13.2"), 77 | &[( 78 | r#"s/^\(afl = {.*\ Result<()> { 107 | run_test( 108 | Some("*"), 109 | &[ 110 | ( 111 | r#"s/^\(version = "\)[^.]*\.[^.]*\.\([^"]*"\)$/\10.0.\2/"#, 112 | &["test-fuzz/Cargo.toml"], 113 | ), 114 | ( 115 | r#"s/^\(test-fuzz = {.*\ Result<()> { 147 | run_test( 148 | Some("*"), 149 | &[ 150 | ( 151 | r#"s/^\(version = "[^.]*\.[^.]*\)\.[^"]*\("\)$/\1.255\2/"#, 152 | &["test-fuzz/Cargo.toml"], 153 | ), 154 | ( 155 | r#"s/^\(test-fuzz = {.*\, 172 | sed_scripts_and_paths: &[(&str, &[&str])], 173 | grep_patterns: &[&str], 174 | ) -> Result<()> { 175 | sandbox(|sh| { 176 | if let Some(version) = afl_version { 177 | cmd!(sh, "cargo install cargo-afl --version={version} --quiet").run()?; 178 | } 179 | 180 | for &(script, paths) in sed_scripts_and_paths { 181 | cmd!(sh, "sed -i {script} {paths...}").run()?; 182 | } 183 | 184 | let output = cmd!( 185 | sh, 186 | "cargo run -p cargo-test-fuzz -- test-fuzz -p test-fuzz-examples --no-run" 187 | ) 188 | .ignore_status() 189 | .read_stderr()?; 190 | 191 | println!("{output}"); 192 | 193 | let tempfile = write_to_tempfile(&output)?; 194 | let tempfile_path = tempfile.path(); 195 | 196 | for pattern in grep_patterns { 197 | cmd!(sh, "grep -m 1 {pattern} {tempfile_path}").run()?; 198 | } 199 | 200 | Ok(()) 201 | }) 202 | } 203 | 204 | fn sandbox(f: impl FnOnce(Shell) -> Result<()>) -> Result<()> { 205 | let sh = Shell::new()?; 206 | 207 | let home = sh.create_temp_dir()?; 208 | sh.change_dir(home.path()); 209 | 210 | sh.set_var("PATH", "/usr/bin"); 211 | 212 | install_rust(&sh)?; 213 | 214 | // smoelius: `HOME` can be set only after Rust is installed. See, e.g.: 215 | // https://github.com/rust-lang/rustup/issues/1884#issuecomment-498157692 216 | sh.set_var("HOME", home.path()); 217 | 218 | #[cfg_attr(dylint_lib = "general", allow(abs_home_path))] 219 | let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); 220 | 221 | cmd!(sh, "git clone {repo} workdir").run()?; 222 | 223 | sh.change_dir("workdir"); 224 | 225 | f(sh) 226 | } 227 | 228 | fn install_rust(sh: &Shell) -> Result<()> { 229 | let cargo_home = sh.current_dir().join(".cargo"); 230 | let rustup_home = sh.current_dir().join(".rustup"); 231 | 232 | sh.set_var("CARGO_HOME", &cargo_home); 233 | sh.set_var("RUSTUP_HOME", rustup_home); 234 | 235 | let output = cmd!( 236 | sh, 237 | "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs" 238 | ) 239 | .read()?; 240 | 241 | let tempfile = write_to_tempfile(&output)?; 242 | let tempfile_path = tempfile.path(); 243 | 244 | cmd!( 245 | sh, 246 | "sh {tempfile_path} -y --no-modify-path --profile minimal --quiet" 247 | ) 248 | .run()?; 249 | 250 | let path_old = sh.var("PATH")?; 251 | let path_new = join_paths([format!("{}/bin", cargo_home.to_string_lossy()), path_old])?; 252 | sh.set_var("PATH", path_new); 253 | 254 | Ok(()) 255 | } 256 | 257 | fn write_to_tempfile(contents: &str) -> Result { 258 | let tempfile = NamedTempFile::new()?; 259 | write(&tempfile, contents)?; 260 | Ok(tempfile) 261 | } 262 | -------------------------------------------------------------------------------- /third-party/patches/substrate_client_transaction_pool.patch: -------------------------------------------------------------------------------- 1 | diff --git a/substrate/client/transaction-pool/Cargo.toml b/substrate/client/transaction-pool/Cargo.toml 2 | index e29b3f8..7b878de 100644 3 | --- a/substrate/client/transaction-pool/Cargo.toml 4 | +++ b/substrate/client/transaction-pool/Cargo.toml 5 | @@ -28,10 +28,10 @@ indexmap = { workspace = true } 6 | itertools = { workspace = true } 7 | linked-hash-map = { workspace = true } 8 | -parking_lot = { workspace = true, default-features = true } 9 | +parking_lot = { workspace = true, features = ["serde"] } 10 | prometheus-endpoint = { workspace = true, default-features = true } 11 | sc-client-api = { workspace = true, default-features = true } 12 | sc-transaction-pool-api = { workspace = true, default-features = true } 13 | sc-utils = { workspace = true, default-features = true } 14 | -serde = { features = ["derive"], workspace = true, default-features = true } 15 | +serde = { features = ["derive", "rc"], workspace = true, default-features = true } 16 | sp-api = { workspace = true, default-features = true } 17 | sp-blockchain = { workspace = true, default-features = true } 18 | @@ -47,4 +47,6 @@ tokio-stream = { workspace = true } 19 | tracing = { workspace = true, default-features = true } 20 | 21 | +test-fuzz = { path = "../../../../../test-fuzz" } 22 | + 23 | [dev-dependencies] 24 | anyhow = { workspace = true } 25 | diff --git a/substrate/client/transaction-pool/src/graph/base_pool.rs b/substrate/client/transaction-pool/src/graph/base_pool.rs 26 | index 72ec192..2837e3b 100644 27 | --- a/substrate/client/transaction-pool/src/graph/base_pool.rs 28 | +++ b/substrate/client/transaction-pool/src/graph/base_pool.rs 29 | @@ -85,5 +85,5 @@ pub struct PruneStatus { 30 | 31 | /// A transaction source that includes a timestamp indicating when the transaction was submitted. 32 | -#[derive(Debug, Clone, PartialEq, Eq)] 33 | +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] 34 | pub struct TimedTransactionSource { 35 | /// The original source of the transaction. 36 | @@ -91,4 +91,5 @@ pub struct TimedTransactionSource { 37 | 38 | /// The time at which the transaction was submitted. 39 | + #[serde(skip)] 40 | pub timestamp: Option, 41 | } 42 | @@ -123,5 +124,5 @@ impl TimedTransactionSource { 43 | 44 | /// Immutable transaction 45 | -#[derive(PartialEq, Eq, Clone)] 46 | +#[derive(PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)] 47 | pub struct Transaction { 48 | /// Raw extrinsic representing that transaction. 49 | @@ -259,5 +260,7 @@ pub struct BasePool { 50 | } 51 | 52 | -impl Default for BasePool { 53 | +impl Default 54 | + for BasePool 55 | +{ 56 | fn default() -> Self { 57 | Self::new(false) 58 | @@ -265,5 +268,7 @@ impl Default for Bas 59 | } 60 | 61 | -impl BasePool { 62 | +impl 63 | + BasePool 64 | +{ 65 | /// Create new pool given reject_future_transactions flag. 66 | pub fn new(reject_future_transactions: bool) -> Self { 67 | diff --git a/substrate/client/transaction-pool/src/graph/future.rs b/substrate/client/transaction-pool/src/graph/future.rs 68 | index 848893b..8742168 100644 69 | --- a/substrate/client/transaction-pool/src/graph/future.rs 70 | +++ b/substrate/client/transaction-pool/src/graph/future.rs 71 | @@ -30,4 +30,5 @@ use super::base_pool::Transaction; 72 | use crate::{common::tracing_log_xt::log_xt_trace, LOG_TARGET}; 73 | 74 | +#[derive(serde::Deserialize, serde::Serialize)] 75 | /// Transaction with partially satisfied dependencies. 76 | pub struct WaitingTransaction { 77 | @@ -37,4 +38,5 @@ pub struct WaitingTransaction { 78 | pub missing_tags: HashSet, 79 | /// Time of import to the Future Queue. 80 | + #[serde(skip, default = "Instant::now")] 81 | pub imported_at: Instant, 82 | } 83 | diff --git a/substrate/client/transaction-pool/src/graph/ready.rs b/substrate/client/transaction-pool/src/graph/ready.rs 84 | index 88986ba..0ac9f7e 100644 85 | --- a/substrate/client/transaction-pool/src/graph/ready.rs 86 | +++ b/substrate/client/transaction-pool/src/graph/ready.rs 87 | @@ -39,5 +39,5 @@ use super::{ 88 | /// 89 | /// Should be cheap to clone. 90 | -#[derive(Debug)] 91 | +#[derive(Debug, serde::Deserialize, serde::Serialize)] 92 | pub struct TransactionRef { 93 | /// The actual transaction data. 94 | @@ -76,5 +76,5 @@ impl PartialEq for TransactionRef { 95 | impl Eq for TransactionRef {} 96 | 97 | -#[derive(Debug)] 98 | +#[derive(Debug, serde::Deserialize, serde::Serialize)] 99 | pub struct ReadyTx { 100 | /// A reference to a transaction 101 | @@ -107,5 +107,5 @@ qed 102 | 103 | /// Validated transactions that are block ready with all their dependencies met. 104 | -#[derive(Clone, Debug)] 105 | +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 106 | pub struct ReadyTransactions { 107 | /// Next free insertion id (used to indicate when a transaction was inserted into the pool). 108 | @@ -138,5 +138,10 @@ impl Default for ReadyTransactions { 109 | } 110 | 111 | -impl ReadyTransactions { 112 | +#[test_fuzz::test_fuzz_impl] 113 | +impl< 114 | + Hash: std::fmt::Debug + hash::Hash + Member + Serialize, 115 | + Ex: Clone + std::fmt::Debug + Serialize, 116 | + > ReadyTransactions 117 | +{ 118 | /// Borrows a map of tags that are provided by transactions in this queue. 119 | pub fn provided_tags(&self) -> &HashMap { 120 | @@ -176,8 +181,12 @@ impl ReadyTransactions { 121 | /// that are in this queue. 122 | /// Returns transactions that were replaced by the one imported. 123 | + #[test_fuzz::test_fuzz(impl_generic_args = "u64, Vec", bounds = "Hash: Eq + hash::Hash")] 124 | pub fn import( 125 | &mut self, 126 | tx: WaitingTransaction, 127 | ) -> error::Result>>> { 128 | + if !tx.is_ready() { 129 | + return Ok(Vec::default()); 130 | + } 131 | assert!( 132 | tx.is_ready(), 133 | @@ -185,4 +194,7 @@ impl ReadyTransactions { 134 | tx.missing_tags 135 | ); 136 | + if self.ready.read().contains_key(&tx.transaction.hash) { 137 | + return Ok(Vec::default()); 138 | + } 139 | assert!( 140 | !self.ready.read().contains_key(&tx.transaction.hash), 141 | @@ -204,4 +216,7 @@ impl ReadyTransactions { 142 | // Check if the transaction that satisfies the tag is still in the queue. 143 | if let Some(other) = self.provided_tags.get(tag) { 144 | + if ready.get_mut(other).is_none() { 145 | + return Ok(Vec::default()); 146 | + } 147 | let tx = ready.get_mut(other).expect(HASH_READY); 148 | tx.unlocks.push(hash.clone()); 149 | @@ -604,5 +619,8 @@ mod tests { 150 | } 151 | 152 | - fn import( 153 | + fn import< 154 | + H: std::fmt::Debug + hash::Hash + Eq + Member + Serialize, 155 | + Ex: Clone + std::fmt::Debug + Serialize, 156 | + >( 157 | ready: &mut ReadyTransactions, 158 | tx: Transaction, 159 | diff --git a/substrate/client/transaction-pool/src/graph/tracked_map.rs b/substrate/client/transaction-pool/src/graph/tracked_map.rs 160 | index 644ed04..624bf99 100644 161 | --- a/substrate/client/transaction-pool/src/graph/tracked_map.rs 162 | +++ b/substrate/client/transaction-pool/src/graph/tracked_map.rs 163 | @@ -34,6 +34,9 @@ pub trait Size { 164 | /// 165 | /// Size reported might be slightly off and only approximately true. 166 | -#[derive(Debug)] 167 | -pub struct TrackedMap { 168 | +#[derive(Debug, serde::Deserialize, serde::Serialize)] 169 | +pub struct TrackedMap 170 | +where 171 | + K: Eq + std::hash::Hash, 172 | +{ 173 | index: Arc>>, 174 | bytes: AtomicIsize, 175 | @@ -41,5 +44,8 @@ pub struct TrackedMap { 176 | } 177 | 178 | -impl Default for TrackedMap { 179 | +impl Default for TrackedMap 180 | +where 181 | + K: Eq + std::hash::Hash, 182 | +{ 183 | fn default() -> Self { 184 | Self { index: Arc::new(HashMap::default().into()), bytes: 0.into(), length: 0.into() } 185 | @@ -49,5 +55,5 @@ impl Default for TrackedMap { 186 | impl Clone for TrackedMap 187 | where 188 | - K: Clone, 189 | + K: Clone + Eq + std::hash::Hash, 190 | V: Clone, 191 | { 192 | @@ -61,5 +67,8 @@ where 193 | } 194 | 195 | -impl TrackedMap { 196 | +impl TrackedMap 197 | +where 198 | + K: Eq + std::hash::Hash, 199 | +{ 200 | /// Current tracked length of the content. 201 | pub fn len(&self) -> usize { 202 | @@ -87,5 +96,8 @@ impl TrackedMap { 203 | } 204 | 205 | -impl TrackedMap { 206 | +impl TrackedMap 207 | +where 208 | + K: Eq + std::hash::Hash, 209 | +{ 210 | /// Clone the inner map. 211 | pub fn clone_map(&self) -> HashMap { 212 | diff --git a/substrate/primitives/runtime/src/transaction_validity.rs b/substrate/primitives/runtime/src/transaction_validity.rs 213 | index f7ef143..ea91459 100644 214 | --- a/substrate/primitives/runtime/src/transaction_validity.rs 215 | +++ b/substrate/primitives/runtime/src/transaction_validity.rs 216 | @@ -246,4 +246,5 @@ impl From for TransactionValidity { 217 | /// by our local node (for instance off-chain workers). 218 | #[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo, Hash)] 219 | +#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] 220 | pub enum TransactionSource { 221 | /// Transaction is already included in block. 222 | -------------------------------------------------------------------------------- /test-fuzz/tests/integration/ci.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::assert::OutputAssertExt; 2 | use regex::Regex; 3 | use similar_asserts::SimpleDiff; 4 | use std::{ 5 | env, 6 | ffi::OsStr, 7 | fs::{read_dir, read_to_string, write}, 8 | io::Write, 9 | ops::Range, 10 | path::Path, 11 | process::{Command, ExitStatus}, 12 | str::FromStr, 13 | }; 14 | use tempfile::tempdir; 15 | use testing::CommandExt; 16 | use walkdir::WalkDir; 17 | 18 | #[test] 19 | fn actionlint() { 20 | let mut command = Command::new("which"); 21 | command.arg("actionlint"); 22 | let output = command.output().unwrap(); 23 | if !output.status.success() { 24 | #[allow(clippy::explicit_write)] 25 | writeln!( 26 | std::io::stderr(), 27 | "Skipping `actionlint` test as `actionlint` is unavailable" 28 | ) 29 | .unwrap(); 30 | return; 31 | } 32 | let stdout = String::from_utf8(output.stdout).unwrap(); 33 | Command::new(stdout.trim_end()) 34 | .env("SHELLCHECK_OPTS", "--exclude=SC2002") 35 | .logged_assert() 36 | .success(); 37 | } 38 | 39 | #[test] 40 | fn clippy() { 41 | Command::new("cargo") 42 | .args(["clippy", "--all-targets", "--", "--deny=warnings"]) 43 | .logged_assert() 44 | .success(); 45 | } 46 | 47 | #[test] 48 | fn dylint() { 49 | Command::new("cargo") 50 | .args(["dylint", "--all", "--", "--all-targets"]) 51 | .env("DYLINT_RUSTFLAGS", "--deny warnings") 52 | .logged_assert() 53 | .success(); 54 | } 55 | 56 | #[test] 57 | fn fmt() { 58 | Command::new("cargo") 59 | .args(["+nightly", "fmt", "--check"]) 60 | .current_dir("..") 61 | .logged_assert() 62 | .success(); 63 | } 64 | 65 | #[test] 66 | fn license() { 67 | let re = Regex::new(r"^[^:]*\b(Apache-2.0|BSD-2-Clause|BSD-3-Clause|ISC|MIT|N/A)\b").unwrap(); 68 | 69 | for line in std::str::from_utf8( 70 | &Command::new("cargo") 71 | .arg("license") 72 | .current_dir("..") 73 | .logged_assert() 74 | .success() 75 | .get_output() 76 | .stdout, 77 | ) 78 | .unwrap() 79 | .lines() 80 | { 81 | if line 82 | == "AGPL-3.0 WITH mif-exception (5): cargo-test-fuzz, test-fuzz, test-fuzz-internal, \ 83 | test-fuzz-macro, test-fuzz-runtime" 84 | { 85 | continue; 86 | } 87 | assert!(re.is_match(line), "{line:?} does not match"); 88 | } 89 | } 90 | 91 | #[test] 92 | fn markdown_link_check() { 93 | let tempdir = tempdir().unwrap(); 94 | 95 | Command::new("npm") 96 | .args(["install", "markdown-link-check"]) 97 | .current_dir(&tempdir) 98 | .logged_assert() 99 | .success(); 100 | 101 | let readme_md = Path::new(env!("CARGO_MANIFEST_DIR")).join("../README.md"); 102 | 103 | Command::new("npx") 104 | .args(["markdown-link-check", readme_md.to_str().unwrap()]) 105 | .current_dir(&tempdir) 106 | .logged_assert() 107 | .success(); 108 | } 109 | 110 | #[test] 111 | fn prettier() { 112 | Command::new("prettier") 113 | .args([ 114 | "--check", 115 | "**/*.json", 116 | "**/*.md", 117 | "**/*.yml", 118 | "!test-fuzz/tests/supply_chain.json", 119 | ]) 120 | .current_dir("..") 121 | .logged_assert() 122 | .success(); 123 | } 124 | 125 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 126 | #[test] 127 | fn readme_contains_usage() { 128 | let readme = read_to_string("../README.md").unwrap(); 129 | 130 | let assert = Command::new("cargo") 131 | .args(["run", "--bin=cargo-test-fuzz", "--", "test-fuzz", "--help"]) 132 | .logged_assert(); 133 | let stdout = &assert.get_output().stdout; 134 | 135 | let usage = std::str::from_utf8(stdout) 136 | .unwrap() 137 | .split_inclusive('\n') 138 | .skip(2) 139 | .collect::(); 140 | 141 | assert!( 142 | readme.contains(&usage), 143 | "{}", 144 | SimpleDiff::from_str(&readme, &usage, "left", "right") 145 | ); 146 | } 147 | 148 | #[test] 149 | fn readme_does_not_use_inline_links() { 150 | let readme = read_to_string("../README.md").unwrap(); 151 | assert!( 152 | !Regex::new(r"\[[^\]]*\]\(").unwrap().is_match(&readme), 153 | "readme uses inline links", 154 | ); 155 | } 156 | 157 | #[test] 158 | fn readme_reference_links_are_sorted() { 159 | let re = Regex::new(r"^\[[^\]]*\]:").unwrap(); 160 | let readme = read_to_string("../README.md").unwrap(); 161 | let links = readme 162 | .lines() 163 | .filter(|line| re.is_match(line)) 164 | .collect::>(); 165 | let mut links_sorted = links.clone(); 166 | links_sorted.sort_unstable(); 167 | assert_eq!(links_sorted, links); 168 | } 169 | 170 | #[test] 171 | fn shellcheck() { 172 | for entry in read_dir("../scripts").unwrap() { 173 | let entry = entry.unwrap(); 174 | let path = entry.path(); 175 | if path.is_file() { 176 | Command::new("shellcheck") 177 | .args(["--exclude=SC2002", &path.to_string_lossy()]) 178 | .logged_assert() 179 | .success(); 180 | } 181 | } 182 | } 183 | 184 | #[test] 185 | fn dependencies_are_sorted() { 186 | for entry in WalkDir::new("..") 187 | .into_iter() 188 | .filter_map(Result::ok) 189 | .filter(|e| e.file_name() == OsStr::new("Cargo.toml")) 190 | { 191 | let path = entry.path(); 192 | let contents = read_to_string(path).unwrap(); 193 | let document = contents.parse::>().unwrap(); 194 | for table_name in ["dependencies", "dev-dependencies", "build-dependencies"] { 195 | let Some(span) = key_value_pair_span(&document, table_name) else { 196 | continue; 197 | }; 198 | assert!( 199 | key_value_pairs_are_sorted(&document, span), 200 | "`{table_name}` in `{}` are not sorted", 201 | path.display() 202 | ); 203 | } 204 | } 205 | } 206 | 207 | fn key_value_pair_span( 208 | document: &toml_edit::Document, 209 | table_name: &str, 210 | ) -> Option> { 211 | // smoelius: The table might not exist. 212 | let item = document.get(table_name)?; 213 | let table = item.as_table().unwrap(); 214 | // smoelius: The table might exist but be empty. 215 | let (_, last_item) = table.iter().last()?; 216 | let header_span = table.span().unwrap(); 217 | let last_item_span = last_item.span().unwrap(); 218 | Some(header_span.end..last_item_span.end) 219 | } 220 | 221 | fn key_value_pairs_are_sorted>( 222 | document: &toml_edit::Document, 223 | span: Range, 224 | ) -> bool { 225 | for group in groups(document, span) { 226 | let pairs = &document.raw()[group] 227 | .parse::>() 228 | .unwrap(); 229 | if !pairs.iter().map(|(k, _)| k).is_sorted() { 230 | return false; 231 | } 232 | } 233 | true 234 | } 235 | 236 | fn groups>( 237 | document: &toml_edit::Document, 238 | span: Range, 239 | ) -> Vec> { 240 | let group_starts = group_starts(document, &span); 241 | let mut groups = Vec::with_capacity(group_starts.len() + 1); 242 | let mut start = span.start; 243 | for partition in group_starts { 244 | groups.push(start..partition); 245 | start = partition; 246 | } 247 | groups.push(start..span.end); 248 | groups 249 | } 250 | 251 | /// Find the offsets in `span` that are not newlines, but that are preceded by two (or more) 252 | /// newlines. 253 | fn group_starts>( 254 | document: &toml_edit::Document, 255 | span: &Range, 256 | ) -> Vec { 257 | let raw = &document.raw()[span.clone()].as_bytes(); 258 | (2..raw.len()) 259 | .filter(|&i| raw[i - 2] == b'\n' && raw[i - 1] == b'\n' && raw[i] != b'\n') 260 | .map(|i| span.start + i) 261 | .collect() 262 | } 263 | 264 | // smoelius: No other test uses supply_chain.json. 265 | #[allow(clippy::disallowed_methods)] 266 | #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))] 267 | #[test] 268 | fn supply_chain() { 269 | let mut command = Command::new("cargo"); 270 | command.args(["supply-chain", "update", "--cache-max-age=0s"]); 271 | let _: ExitStatus = command.status().unwrap(); 272 | 273 | let mut command = Command::new("cargo"); 274 | command.args(["supply-chain", "json", "--no-dev"]); 275 | let assert = command.assert().success(); 276 | 277 | let stdout_actual = std::str::from_utf8(&assert.get_output().stdout).unwrap(); 278 | let mut value = serde_json::Value::from_str(stdout_actual).unwrap(); 279 | remove_avatars(&mut value); 280 | let stdout_normalized = serde_json::to_string_pretty(&value).unwrap(); 281 | 282 | let path_buf = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/supply_chain.json"); 283 | 284 | if enabled("BLESS") { 285 | write(path_buf, stdout_normalized).unwrap(); 286 | } else { 287 | let stdout_expected = read_to_string(&path_buf).unwrap(); 288 | 289 | assert!( 290 | stdout_expected == stdout_normalized, 291 | "{}", 292 | SimpleDiff::from_str(&stdout_expected, &stdout_normalized, "left", "right") 293 | ); 294 | } 295 | } 296 | 297 | fn remove_avatars(value: &mut serde_json::Value) { 298 | match value { 299 | serde_json::Value::Null 300 | | serde_json::Value::Bool(_) 301 | | serde_json::Value::Number(_) 302 | | serde_json::Value::String(_) => {} 303 | serde_json::Value::Array(array) => { 304 | for value in array { 305 | remove_avatars(value); 306 | } 307 | } 308 | serde_json::Value::Object(object) => { 309 | object.retain(|key, value| { 310 | if key == "avatar" { 311 | return false; 312 | } 313 | remove_avatars(value); 314 | true 315 | }); 316 | } 317 | } 318 | } 319 | 320 | #[test] 321 | fn udeps() { 322 | Command::new("cargo") 323 | .args([ 324 | "+nightly", 325 | "udeps", 326 | "--features=test-install", 327 | "--all-targets", 328 | ]) 329 | .current_dir("..") 330 | .logged_assert() 331 | .success(); 332 | } 333 | 334 | #[test] 335 | fn unmaintained() { 336 | Command::new("cargo") 337 | .args(["unmaintained", "--color=never", "--fail-fast"]) 338 | .logged_assert() 339 | .success(); 340 | } 341 | 342 | #[must_use] 343 | pub fn enabled(key: &str) -> bool { 344 | env::var(key).is_ok_and(|value| value != "0") 345 | } 346 | -------------------------------------------------------------------------------- /third-party/tests/third_party.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::assert::Assert; 2 | use cargo_metadata::MetadataCommand; 3 | use log::debug; 4 | use option_set::option_set; 5 | use predicates::prelude::*; 6 | use rustc_version::{Channel, version_meta}; 7 | use serde::Deserialize; 8 | use std::{ 9 | ffi::OsStr, 10 | fs::read_to_string, 11 | io::{Write, stderr, stdout}, 12 | path::Path, 13 | process::Command, 14 | sync::LazyLock, 15 | }; 16 | use tempfile::tempdir_in; 17 | use testing::CommandExt; 18 | 19 | option_set! { 20 | struct Flags: UpperSnake + u8 { 21 | const EXPENSIVE = 1 << 0; 22 | // const SKIP = 1 << 1; 23 | const SKIP_NIGHTLY = 1 << 2; 24 | } 25 | } 26 | 27 | #[derive(Deserialize)] 28 | struct Test { 29 | flags: Flags, 30 | url: String, 31 | rev: String, 32 | patch: String, 33 | subdir: String, 34 | test_package: String, 35 | fuzz_package: String, 36 | targets: Vec, 37 | } 38 | 39 | static TESTS: LazyLock> = LazyLock::new(|| { 40 | #[cfg_attr(dylint_lib = "general", allow(abs_home_path))] 41 | let content = 42 | read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("third_party.json")).unwrap(); 43 | serde_json::from_str(&content).unwrap() 44 | }); 45 | 46 | #[cfg_attr(dylint_lib = "supplementary", allow(commented_out_code))] 47 | #[test] 48 | fn test() { 49 | let version_meta = version_meta().unwrap(); 50 | for test in TESTS.iter() { 51 | run_test( 52 | test, 53 | (!cfg!(feature = "test-third-party-full") && test.flags.contains(Flags::EXPENSIVE)) 54 | // || test.flags.contains(Flags::SKIP) 55 | || (test.flags.contains(Flags::SKIP_NIGHTLY) 56 | && version_meta.channel == Channel::Nightly), 57 | ); 58 | } 59 | } 60 | 61 | #[allow(clippy::too_many_lines)] 62 | fn run_test(test: &Test, no_run: bool) { 63 | #[allow(clippy::explicit_write)] 64 | writeln!( 65 | stderr(), 66 | "{}{}", 67 | test.url, 68 | if no_run { " (no-run)" } else { "" } 69 | ) 70 | .unwrap(); 71 | 72 | // smoelius: Each patch expects test-fuzz to be an ancestor of the directory in which the patch 73 | // is applied. 74 | #[cfg_attr(dylint_lib = "general", allow(abs_home_path))] 75 | let tempdir = tempdir_in(env!("CARGO_MANIFEST_DIR")).unwrap(); 76 | 77 | Command::new("git") 78 | .current_dir(&tempdir) 79 | .args(["clone", &test.url, "."]) 80 | .logged_assert() 81 | .success(); 82 | 83 | Command::new("git") 84 | .current_dir(&tempdir) 85 | .args(["checkout", &test.rev]) 86 | .logged_assert() 87 | .success(); 88 | 89 | #[cfg_attr(dylint_lib = "general", allow(abs_home_path))] 90 | let patch = Path::new(env!("CARGO_MANIFEST_DIR")) 91 | .join("patches") 92 | .join(&test.patch) 93 | .canonicalize() 94 | .unwrap(); 95 | 96 | Command::new("git") 97 | .current_dir(&tempdir) 98 | .args(["apply", &patch.to_string_lossy()]) 99 | .logged_assert() 100 | .success(); 101 | 102 | let subdir = tempdir.path().join(&test.subdir); 103 | 104 | // smoelius: Right now, Substrate's lockfile refers to `pin-project:0.4.27`, which is 105 | // incompatible with `syn:1.0.84`. 106 | // smoelius: The `pin-project` issue has been resolved. But Substrate now chokes when 107 | // `libp2p-swarm-derive` is upgraded from 0.30.1 to 0.30.2. 108 | // smoelius: Substrate now chokes when `x25519-dalek` is upgraded from 2.0.0-pre.1 to 2.0.0. 109 | // However, rather than try to upgrade everything and then downgrade just `x25519-dalek` 110 | // (similar to how I did for `libp2p-swarm-derive`), I am instead trying to upgrade just the 111 | // packages that need it. 112 | // smoelius: `cw20-base` relies on `serde_json` 1.0.114, which is before `serde_json::Value` 113 | // started deriving `Hash`: 114 | // https://github.com/serde-rs/json/blob/e1b3a6d8a161ff5ec4865b487d148c17d0188e3e/src/value/mod.rs#L115 115 | for package in [ 116 | "ahash@0.7.6", 117 | "ahash@0.7.7", 118 | "libc", 119 | "num-bigint@0.4.0", 120 | "serde_json", 121 | "tempfile", 122 | "wasm-bindgen", 123 | ] { 124 | #[allow(clippy::let_unit_value)] 125 | let () = Command::new("cargo") 126 | .current_dir(&subdir) 127 | .args(["update", "-p", package]) 128 | .logged_assert() 129 | .try_success() 130 | .map_or((), |_| ()); 131 | } 132 | 133 | // smoelius: The `libp2p-swarm-derive` issue appears to have been resolved. 134 | #[cfg(any())] 135 | { 136 | let mut command = Command::new("cargo"); 137 | command 138 | .current_dir(&subdir) 139 | .args(["update", "-p", "libp2p-swarm-derive"]); 140 | if command.assert().try_success().is_ok() { 141 | command.args(["--precise", "0.30.1"]).assert().success(); 142 | } 143 | } 144 | 145 | check_test_fuzz_dependency(&subdir, &test.test_package); 146 | check_test_fuzz_dependency(&subdir, &test.fuzz_package); 147 | 148 | if no_run { 149 | return; 150 | } 151 | 152 | // smoelius: Use `std::process::Command` so that we can see the output of the command as it 153 | // runs. `assert_cmd::Command` would capture the output. 154 | for flags in [&["--no-run", "--quiet"], &[]] as [&[&str]; 2] { 155 | let mut command = Command::new("cargo"); 156 | command 157 | .current_dir(&subdir) 158 | .env_remove("RUSTUP_TOOLCHAIN") 159 | .env("TEST_FUZZ_WRITE", "1") 160 | .args([ 161 | "test", 162 | "--package", 163 | &test.test_package, 164 | "--features", 165 | &("test-fuzz/".to_owned() + test_fuzz::serde_format::as_feature()), 166 | ]) 167 | .args(flags) 168 | .args(["--", "--nocapture"]); 169 | debug!("{command:?}"); 170 | assert!(command.status().unwrap().success()); 171 | } 172 | 173 | for target in &test.targets { 174 | test_fuzz(&subdir, &test.fuzz_package, target, ["--display=corpus"]) 175 | .stdout(predicate::str::is_match(r"(?m)^[[:xdigit:]]{40}:").unwrap()) 176 | .stdout(predicate::str::contains("unknown of type").not()); 177 | 178 | test_fuzz(&subdir, &test.fuzz_package, target, ["--replay=corpus"]).stdout( 179 | predicate::str::is_match(r"(?m)^[[:xdigit:]]{40}: Ret\((Ok|Err)\(.*\)\)$").unwrap(), 180 | ); 181 | } 182 | } 183 | 184 | fn check_test_fuzz_dependency(subdir: &Path, package_name: &str) { 185 | let metadata = MetadataCommand::new() 186 | .current_dir(subdir) 187 | .no_deps() 188 | .exec() 189 | .unwrap(); 190 | let package = metadata 191 | .packages 192 | .iter() 193 | .find(|package| package.name == package_name) 194 | .unwrap_or_else(|| panic!("Could not find package `{package_name}`")); 195 | let dep = package 196 | .dependencies 197 | .iter() 198 | .find(|dep| dep.name == "test-fuzz") 199 | .expect("Could not find dependency `test-fuzz`"); 200 | assert!(dep.path.is_some()); 201 | } 202 | 203 | fn test_fuzz(subdir: P, package: &str, target: &str, args: I) -> Assert 204 | where 205 | P: AsRef, 206 | I: IntoIterator, 207 | S: AsRef, 208 | { 209 | Command::new("cargo") 210 | .args(["build", "--package=cargo-test-fuzz"]) 211 | .logged_assert() 212 | .success(); 213 | 214 | let metadata = MetadataCommand::new().no_deps().exec().unwrap(); 215 | 216 | let assert = Command::new(metadata.target_directory.join("debug/cargo-test-fuzz")) 217 | .current_dir(subdir) 218 | .env("RUST_LOG", "debug") 219 | .args([ 220 | "test-fuzz", 221 | "--package", 222 | package, 223 | "--features", 224 | &("test-fuzz/".to_owned() + test_fuzz::serde_format::as_feature()), 225 | target, 226 | ]) 227 | .args(args) 228 | .logged_assert() 229 | .success(); 230 | stderr().write_all(&assert.get_output().stderr).unwrap(); 231 | stdout().write_all(&assert.get_output().stdout).unwrap(); 232 | assert 233 | } 234 | 235 | #[cfg(feature = "test-third-party-full")] 236 | #[test] 237 | fn patches_are_current() { 238 | // smoelius: This should match `scripts/update_patches.sh`. 239 | const LINES_OF_CONTEXT: u32 = 2; 240 | 241 | let re = regex::Regex::new(r"^index [[:xdigit:]]{7}\.\.[[:xdigit:]]{7} [0-7]{6}$").unwrap(); 242 | 243 | for test in TESTS.iter() { 244 | let tempdir = tempdir_in(env!("CARGO_MANIFEST_DIR")).unwrap(); 245 | 246 | Command::new("git") 247 | .current_dir(&tempdir) 248 | .args(["clone", "--depth=1", &test.url, "."]) 249 | .logged_assert() 250 | .success(); 251 | 252 | let patch_path = Path::new(env!("CARGO_MANIFEST_DIR")) 253 | .join("patches") 254 | .join(&test.patch); 255 | let patch = read_to_string(patch_path).unwrap(); 256 | 257 | // smoelius: To use `std::process::Command` here, the `write_stdin` would have to be 258 | // removed. 259 | #[allow(clippy::disallowed_methods)] 260 | assert_cmd::Command::new("git") 261 | .current_dir(&tempdir) 262 | .args(["apply"]) 263 | .write_stdin(patch.as_bytes()) 264 | .assert() 265 | .success(); 266 | 267 | // smoelius: The following checks are *not* redundant. They can fail even if the patch 268 | // applies. 269 | 270 | // smoelius: Don't use `logged_assert` here. It now calls `strip_ansi_escapes::strip` which 271 | // escapes certain whitespace characters and mess up the diff. 272 | #[allow(clippy::disallowed_methods)] 273 | let assert = assert_cmd::Command::new("git") 274 | .current_dir(&tempdir) 275 | .args(["diff", &format!("--unified={LINES_OF_CONTEXT}")]) 276 | .assert() 277 | .success(); 278 | 279 | let diff = std::str::from_utf8(&assert.get_output().stdout).unwrap(); 280 | 281 | let patch_lines = patch.lines().collect::>(); 282 | let diff_lines = diff.lines().collect::>(); 283 | 284 | assert_eq!(patch_lines.len(), diff_lines.len()); 285 | 286 | for (patch_line, diff_line) in patch_lines.into_iter().zip(diff_lines) { 287 | if !(re.is_match(patch_line) && re.is_match(diff_line)) { 288 | assert_eq!(patch_line, diff_line); 289 | } 290 | } 291 | } 292 | } 293 | 294 | // smoelius: I am disabling this test, as it creates a race to get the patches committed after they 295 | // are updated. 296 | // Note that `patches_are_current` above remains, and that it tests each patch against the head of 297 | // its respective repository, not the revision against which the patch was generated. 298 | #[cfg(any())] 299 | #[test] 300 | #[ignore] 301 | fn revisions_are_current() { 302 | for test in TESTS.iter() { 303 | let tempdir = tempdir_in(env!("CARGO_MANIFEST_DIR")).unwrap(); 304 | 305 | Command::new("git") 306 | .current_dir(&tempdir) 307 | .args(["clone", "--depth=1", &test.url, "."]) 308 | .logged_assert() 309 | .success(); 310 | 311 | let assert = Command::new("git") 312 | .current_dir(&tempdir) 313 | .args(["rev-parse", "HEAD"]) 314 | .logged_assert() 315 | .success(); 316 | 317 | let stdout = std::str::from_utf8(&assert.get_output().stdout).unwrap(); 318 | 319 | assert_eq!(stdout.trim_end(), test.rev); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /examples/tests/serde.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(dylint_lib = "general", allow(crate_wide_allow))] 2 | #![allow(clippy::default_constructed_unit_structs)] 3 | #![allow(clippy::disallowed_names)] 4 | #![allow(clippy::ptr_arg)] 5 | #![allow(clippy::too_many_arguments)] 6 | #![allow(clippy::unit_arg)] 7 | 8 | fn consume(_: T) {} 9 | 10 | mod primitive { 11 | // smoelius: `no_auto_generate` because `serde_cbor` does not fully support 128-bit integers: 12 | // https://github.com/pyfisch/cbor/pull/145 13 | // We might use `ciborium` as an alternative to `serde_cbor`. But `ciborium` currently has no 14 | // way to limit the size of an allocation: https://github.com/enarx/ciborium/issues/11 15 | // smoelius: `serde_cbor` is no longer a supported format. 16 | // #[test_fuzz::test_fuzz(no_auto_generate)] 17 | fn target( 18 | bool: bool, 19 | i8: i8, 20 | i16: i16, 21 | i32: i32, 22 | i64: i64, 23 | i128: i128, 24 | u8: u8, 25 | u16: u16, 26 | u32: u32, 27 | u64: u64, 28 | u128: u128, 29 | f32: f32, 30 | f64: f64, 31 | char: char, 32 | ) { 33 | super::consume(bool); 34 | super::consume(i8); 35 | super::consume(i16); 36 | super::consume(i32); 37 | super::consume(i64); 38 | super::consume(i128); 39 | super::consume(u8); 40 | super::consume(u16); 41 | super::consume(u32); 42 | super::consume(u64); 43 | super::consume(u128); 44 | super::consume(f32); 45 | super::consume(f64); 46 | super::consume(char); 47 | } 48 | 49 | #[test] 50 | fn test() { 51 | target( 52 | bool::default(), 53 | i8::default(), 54 | i16::default(), 55 | i32::default(), 56 | i64::default(), 57 | i128::default(), 58 | u8::default(), 59 | u16::default(), 60 | u32::default(), 61 | u64::default(), 62 | u128::default(), 63 | f32::default(), 64 | f64::default(), 65 | char::default(), 66 | ); 67 | } 68 | } 69 | 70 | mod string { 71 | #[test_fuzz::test_fuzz] 72 | fn target(str: &str, string: String, ref_string: &String, ref_mut_string: &mut String) { 73 | super::consume(str); 74 | super::consume(string); 75 | super::consume(ref_string); 76 | super::consume(ref_mut_string); 77 | } 78 | 79 | #[test] 80 | fn test() { 81 | target( 82 | <&str>::default(), 83 | String::default(), 84 | &String::default(), 85 | &mut String::default(), 86 | ); 87 | } 88 | } 89 | 90 | mod byte_array { 91 | #[test_fuzz::test_fuzz] 92 | fn target( 93 | byte_array_0: [u8; 0], 94 | ref_byte_array_0: &[u8; 0], 95 | ref_mut_byte_array_0: &mut [u8; 0], 96 | byte_array_1: [u8; 1], 97 | ref_byte_array_1: &[u8; 1], 98 | ref_mut_byte_array_1: &mut [u8; 1], 99 | byte_array_2: [u8; 2], 100 | ref_byte_array_2: &[u8; 2], 101 | ref_mut_byte_array_2: &mut [u8; 2], 102 | ) { 103 | super::consume(byte_array_0); 104 | super::consume(ref_byte_array_0); 105 | super::consume(ref_mut_byte_array_0); 106 | super::consume(byte_array_1); 107 | super::consume(ref_byte_array_1); 108 | super::consume(ref_mut_byte_array_1); 109 | super::consume(byte_array_2); 110 | super::consume(ref_byte_array_2); 111 | super::consume(ref_mut_byte_array_2); 112 | } 113 | 114 | #[test] 115 | fn test() { 116 | target( 117 | <[u8; 0]>::default(), 118 | &<[u8; 0]>::default(), 119 | &mut <[u8; 0]>::default(), 120 | <[u8; 1]>::default(), 121 | &<[u8; 1]>::default(), 122 | &mut <[u8; 1]>::default(), 123 | <[u8; 2]>::default(), 124 | &<[u8; 2]>::default(), 125 | &mut <[u8; 2]>::default(), 126 | ); 127 | } 128 | } 129 | 130 | mod option { 131 | #[allow(clippy::ref_option)] 132 | #[test_fuzz::test_fuzz] 133 | fn target(option: Option, ref_option: &Option, ref_mut_option: &mut Option) { 134 | super::consume(option); 135 | super::consume(ref_option); 136 | super::consume(ref_mut_option); 137 | } 138 | 139 | #[test] 140 | fn test() { 141 | target( 142 | Option::::default(), 143 | &Option::::default(), 144 | &mut Option::::default(), 145 | ); 146 | } 147 | } 148 | 149 | mod unit { 150 | #[test_fuzz::test_fuzz] 151 | fn target(unit: (), ref_unit: &(), ref_mut_unit: &mut ()) { 152 | super::consume(unit); 153 | super::consume(ref_unit); 154 | super::consume(ref_mut_unit); 155 | } 156 | 157 | #[test] 158 | fn test() { 159 | target(<()>::default(), &<()>::default(), &mut <()>::default()); 160 | } 161 | } 162 | 163 | mod unit_struct { 164 | use serde::{Deserialize, Serialize}; 165 | #[derive(Clone, Default, Deserialize, Serialize)] 166 | struct UnitStruct; 167 | 168 | #[test_fuzz::test_fuzz] 169 | fn target( 170 | unit_struct: UnitStruct, 171 | ref_unit_struct: &UnitStruct, 172 | ref_mut_unit_struct: &mut UnitStruct, 173 | ) { 174 | super::consume(unit_struct); 175 | super::consume(ref_unit_struct); 176 | super::consume(ref_mut_unit_struct); 177 | } 178 | 179 | #[test] 180 | fn test() { 181 | target( 182 | UnitStruct::default(), 183 | &UnitStruct::default(), 184 | &mut UnitStruct::default(), 185 | ); 186 | } 187 | } 188 | 189 | mod unit_variant { 190 | #![allow(clippy::use_self)] 191 | 192 | use serde::{Deserialize, Serialize}; 193 | #[derive(Clone, Deserialize, Serialize)] 194 | enum UnitVariant { 195 | A, 196 | B, 197 | } 198 | 199 | #[test_fuzz::test_fuzz] 200 | fn target( 201 | unit_variant: UnitVariant, 202 | ref_unit_variant: &UnitVariant, 203 | ref_mut_unit_variant: &mut UnitVariant, 204 | ) { 205 | super::consume(unit_variant); 206 | super::consume(ref_unit_variant); 207 | super::consume(ref_mut_unit_variant); 208 | } 209 | 210 | #[test] 211 | fn test() { 212 | target(UnitVariant::A, &UnitVariant::B, &mut UnitVariant::B); 213 | } 214 | } 215 | 216 | mod newtype_struct { 217 | use serde::{Deserialize, Serialize}; 218 | #[derive(Clone, Default, Deserialize, Serialize)] 219 | struct NewtypeStruct(u8); 220 | 221 | #[test_fuzz::test_fuzz] 222 | fn target( 223 | newtype_struct: NewtypeStruct, 224 | ref_newtype_struct: &NewtypeStruct, 225 | ref_mut_newtype_struct: &mut NewtypeStruct, 226 | ) { 227 | super::consume(newtype_struct); 228 | super::consume(ref_newtype_struct); 229 | super::consume(ref_mut_newtype_struct); 230 | } 231 | 232 | #[test] 233 | fn test() { 234 | target( 235 | NewtypeStruct::default(), 236 | &NewtypeStruct::default(), 237 | &mut NewtypeStruct::default(), 238 | ); 239 | } 240 | } 241 | 242 | mod newtype_variant { 243 | use serde::{Deserialize, Serialize}; 244 | #[derive(Clone, Deserialize, Serialize)] 245 | enum NewtypeVariant { 246 | N(u8), 247 | } 248 | 249 | impl Default for NewtypeVariant { 250 | fn default() -> Self { 251 | Self::N(u8::default()) 252 | } 253 | } 254 | 255 | #[test_fuzz::test_fuzz] 256 | fn target( 257 | newtype_variant: NewtypeVariant, 258 | ref_newtype_variant: &NewtypeVariant, 259 | ref_mut_newtype_variant: &mut NewtypeVariant, 260 | ) { 261 | super::consume(newtype_variant); 262 | super::consume(ref_newtype_variant); 263 | super::consume(ref_mut_newtype_variant); 264 | } 265 | 266 | #[test] 267 | fn test() { 268 | target( 269 | NewtypeVariant::default(), 270 | &NewtypeVariant::default(), 271 | &mut NewtypeVariant::default(), 272 | ); 273 | } 274 | } 275 | 276 | mod seq { 277 | use std::collections::HashSet; 278 | #[test_fuzz::test_fuzz] 279 | fn target( 280 | seq_slice: &[u8], 281 | seq_vec: Vec, 282 | ref_seq_vec: &Vec, 283 | ref_mut_seq_vec: &mut Vec, 284 | seq_hash_set: HashSet, 285 | ref_seq_hash_set: &HashSet, 286 | ref_mut_seq_hash_set: &mut HashSet, 287 | ) { 288 | super::consume(seq_slice); 289 | super::consume(seq_vec); 290 | super::consume(ref_seq_vec); 291 | super::consume(ref_mut_seq_vec); 292 | super::consume(seq_hash_set); 293 | super::consume(ref_seq_hash_set); 294 | super::consume(ref_mut_seq_hash_set); 295 | } 296 | 297 | #[test] 298 | fn test() { 299 | target( 300 | <&[u8]>::default(), 301 | Vec::::default(), 302 | &Vec::::default(), 303 | &mut Vec::::default(), 304 | HashSet::::default(), 305 | &HashSet::::default(), 306 | &mut HashSet::::default(), 307 | ); 308 | } 309 | } 310 | 311 | mod tuple { 312 | #[test_fuzz::test_fuzz] 313 | fn target( 314 | tuple_u8: (u8,), 315 | ref_tuple_u8: &(u8,), 316 | ref_mut_tuple_u8: &mut (u8,), 317 | tuple_u8_u8: (u8, u8), 318 | ref_tuple_u8_u8: &(u8, u8), 319 | ref_mut_tuple_u8_u8: &mut (u8, u8), 320 | ) { 321 | super::consume(tuple_u8); 322 | super::consume(ref_tuple_u8); 323 | super::consume(ref_mut_tuple_u8); 324 | super::consume(tuple_u8_u8); 325 | super::consume(ref_tuple_u8_u8); 326 | super::consume(ref_mut_tuple_u8_u8); 327 | } 328 | 329 | #[test] 330 | fn test() { 331 | target( 332 | <(u8,)>::default(), 333 | &<(u8,)>::default(), 334 | &mut <(u8,)>::default(), 335 | <(u8, u8)>::default(), 336 | &<(u8, u8)>::default(), 337 | &mut <(u8, u8)>::default(), 338 | ); 339 | } 340 | } 341 | 342 | mod tuple_struct { 343 | use serde::{Deserialize, Serialize}; 344 | #[derive(Clone, Default, Deserialize, Serialize)] 345 | struct TupleStruct(u8, u8, u8); 346 | 347 | #[test_fuzz::test_fuzz] 348 | fn target( 349 | tuple_struct: TupleStruct, 350 | ref_tuple_struct: &TupleStruct, 351 | ref_mut_tuple_struct: &mut TupleStruct, 352 | ) { 353 | super::consume(tuple_struct); 354 | super::consume(ref_tuple_struct); 355 | super::consume(ref_mut_tuple_struct); 356 | } 357 | 358 | #[test] 359 | fn test() { 360 | target( 361 | TupleStruct::default(), 362 | &TupleStruct::default(), 363 | &mut TupleStruct::default(), 364 | ); 365 | } 366 | } 367 | 368 | mod tuple_variant { 369 | use serde::{Deserialize, Serialize}; 370 | #[derive(Clone, Deserialize, Serialize)] 371 | enum TupleVariant { 372 | T(u8, u8), 373 | } 374 | 375 | impl Default for TupleVariant { 376 | fn default() -> Self { 377 | Self::T(u8::default(), u8::default()) 378 | } 379 | } 380 | 381 | #[test_fuzz::test_fuzz] 382 | fn target( 383 | tuple_variant: TupleVariant, 384 | ref_tuple_variant: &TupleVariant, 385 | ref_mut_tuple_variant: &mut TupleVariant, 386 | ) { 387 | super::consume(tuple_variant); 388 | super::consume(ref_tuple_variant); 389 | super::consume(ref_mut_tuple_variant); 390 | } 391 | 392 | #[test] 393 | fn test() { 394 | target( 395 | TupleVariant::default(), 396 | &TupleVariant::default(), 397 | &mut TupleVariant::default(), 398 | ); 399 | } 400 | } 401 | 402 | mod map { 403 | use std::collections::BTreeMap; 404 | #[test_fuzz::test_fuzz] 405 | fn target( 406 | map: BTreeMap, 407 | ref_map: &BTreeMap, 408 | ref_mut_map: &mut BTreeMap, 409 | ) { 410 | super::consume(map); 411 | super::consume(ref_map); 412 | super::consume(ref_mut_map); 413 | } 414 | 415 | #[test] 416 | fn test() { 417 | target( 418 | BTreeMap::::default(), 419 | &BTreeMap::::default(), 420 | &mut BTreeMap::::default(), 421 | ); 422 | } 423 | } 424 | 425 | mod strukt { 426 | use serde::{Deserialize, Serialize}; 427 | #[derive(Clone, Default, Deserialize, Serialize)] 428 | struct Struct { 429 | r: u8, 430 | g: u8, 431 | b: u8, 432 | } 433 | 434 | #[test_fuzz::test_fuzz] 435 | fn target(strukt: Struct, ref_strukt: &Struct, ref_mut_strukt: &mut Struct) { 436 | super::consume(strukt); 437 | super::consume(ref_strukt); 438 | super::consume(ref_mut_strukt); 439 | } 440 | 441 | #[test] 442 | fn test() { 443 | target( 444 | Struct::default(), 445 | &Struct::default(), 446 | &mut Struct::default(), 447 | ); 448 | } 449 | } 450 | 451 | mod struct_variant { 452 | use serde::{Deserialize, Serialize}; 453 | #[derive(Clone, Deserialize, Serialize)] 454 | enum StructVariant { 455 | S { r: u8, g: u8, b: u8 }, 456 | } 457 | 458 | impl Default for StructVariant { 459 | fn default() -> Self { 460 | Self::S { 461 | r: u8::default(), 462 | g: u8::default(), 463 | b: u8::default(), 464 | } 465 | } 466 | } 467 | 468 | #[test_fuzz::test_fuzz] 469 | fn target( 470 | struct_variant: StructVariant, 471 | ref_struct_variant: &StructVariant, 472 | ref_mut_struct_variant: &mut StructVariant, 473 | ) { 474 | super::consume(struct_variant); 475 | super::consume(ref_struct_variant); 476 | super::consume(ref_mut_struct_variant); 477 | } 478 | 479 | #[test] 480 | fn test() { 481 | target( 482 | StructVariant::default(), 483 | &StructVariant::default(), 484 | &mut StructVariant::default(), 485 | ); 486 | } 487 | } 488 | --------------------------------------------------------------------------------