Skip to content

Commit f4745d4

Browse files
authored
feat(test): Enable the test fuzzer for Wasm (#6835)
1 parent 0007992 commit f4745d4

File tree

9 files changed

+113
-124
lines changed

9 files changed

+113
-124
lines changed

Cargo.lock

Lines changed: 8 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,12 @@ jsonrpsee = { version = "0.24.7", features = ["client-core"] }
152152
flate2 = "1.0.24"
153153
color-eyre = "0.6.2"
154154
rand = "0.8.5"
155-
proptest = "1.2.0"
156-
proptest-derive = "0.4.0"
155+
# The `fork` and `timeout` feature doesn't compile with Wasm (wait-timeout doesn't find the `imp` module).
156+
proptest = { version = "1.6.0", default-features = false, features = [
157+
"std",
158+
"bit-set",
159+
] }
160+
proptest-derive = "0.5.0"
157161
rayon = "1.8.0"
158162
sha2 = { version = "0.10.6", features = ["compress"] }
159163
sha3 = "0.10.6"

tooling/fuzzer/src/dictionary/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ pub(super) fn build_dictionary_from_program<F: AcirField>(program: &Program<F>)
3333
constants
3434
}
3535

36+
/// Collect `Field` values used in the opcodes of an ACIR circuit.
3637
fn build_dictionary_from_circuit<F: AcirField>(circuit: &Circuit<F>) -> HashSet<F> {
3738
let mut constants: HashSet<F> = HashSet::new();
3839

40+
/// Pull out all the fields from an expression.
3941
fn insert_expr<F: AcirField>(dictionary: &mut HashSet<F>, expr: &Expression<F>) {
4042
let quad_coefficients = expr.mul_terms.iter().map(|(k, _, _)| *k);
4143
let linear_coefficients = expr.linear_combinations.iter().map(|(k, _)| *k);
@@ -104,6 +106,7 @@ fn build_dictionary_from_circuit<F: AcirField>(circuit: &Circuit<F>) -> HashSet<
104106
constants
105107
}
106108

109+
/// Collect `Field` values used in the opcodes of a Brillig function.
107110
fn build_dictionary_from_unconstrained_function<F: AcirField>(
108111
function: &BrilligBytecode<F>,
109112
) -> HashSet<F> {

tooling/fuzzer/src/lib.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome, FuzzTestResult};
2222

2323
use noirc_artifacts::program::ProgramArtifact;
2424

25-
/// An executor for Noir programs which which provides fuzzing support using [`proptest`].
25+
/// An executor for Noir programs which provides fuzzing support using [`proptest`].
2626
///
2727
/// After instantiation, calling `fuzz` will proceed to hammer the program with
2828
/// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the
@@ -38,22 +38,22 @@ pub struct FuzzedExecutor<E> {
3838
runner: TestRunner,
3939
}
4040

41-
impl<
42-
E: Fn(
43-
&Program<FieldElement>,
44-
WitnessMap<FieldElement>,
45-
) -> Result<WitnessStack<FieldElement>, String>,
46-
> FuzzedExecutor<E>
41+
impl<E> FuzzedExecutor<E>
42+
where
43+
E: Fn(
44+
&Program<FieldElement>,
45+
WitnessMap<FieldElement>,
46+
) -> Result<WitnessStack<FieldElement>, String>,
4747
{
48-
/// Instantiates a fuzzed executor given a testrunner
48+
/// Instantiates a fuzzed executor given a [TestRunner].
4949
pub fn new(program: ProgramArtifact, executor: E, runner: TestRunner) -> Self {
5050
Self { program, executor, runner }
5151
}
5252

5353
/// Fuzzes the provided program.
5454
pub fn fuzz(&self) -> FuzzTestResult {
5555
let dictionary = build_dictionary_from_program(&self.program.bytecode);
56-
let strategy = strategies::arb_input_map(&self.program.abi, dictionary);
56+
let strategy = strategies::arb_input_map(&self.program.abi, &dictionary);
5757

5858
let run_result: Result<(), TestError<InputMap>> =
5959
self.runner.clone().run(&strategy, |input_map| {

tooling/fuzzer/src/strategies/int.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use proptest::{
44
};
55
use rand::Rng;
66

7+
type BinarySearch = proptest::num::i128::BinarySearch;
8+
79
/// Strategy for signed ints (up to i128).
810
/// The strategy combines 2 different strategies, each assigned a specific weight:
911
/// 1. Generate purely random value in a range. This will first choose bit size uniformly (up `bits`
@@ -27,6 +29,7 @@ impl IntStrategy {
2729
Self { bits, edge_weight: 10usize, random_weight: 50usize }
2830
}
2931

32+
/// Generate random values near MIN or the MAX value.
3033
fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
3134
let rng = runner.rng();
3235

@@ -40,16 +43,16 @@ impl IntStrategy {
4043
3 => self.type_max() - offset,
4144
_ => unreachable!(),
4245
};
43-
Ok(proptest::num::i128::BinarySearch::new(start))
46+
Ok(BinarySearch::new(start))
4447
}
4548

4649
fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
4750
let rng = runner.rng();
48-
4951
let start: i128 = rng.gen_range(self.type_min()..=self.type_max());
50-
Ok(proptest::num::i128::BinarySearch::new(start))
52+
Ok(BinarySearch::new(start))
5153
}
5254

55+
/// Maximum allowed positive number.
5356
fn type_max(&self) -> i128 {
5457
if self.bits < 128 {
5558
(1i128 << (self.bits - 1)) - 1
@@ -58,6 +61,7 @@ impl IntStrategy {
5861
}
5962
}
6063

64+
/// Minimum allowed negative number.
6165
fn type_min(&self) -> i128 {
6266
if self.bits < 128 {
6367
-(1i128 << (self.bits - 1))
@@ -68,7 +72,7 @@ impl IntStrategy {
6872
}
6973

7074
impl Strategy for IntStrategy {
71-
type Tree = proptest::num::i128::BinarySearch;
75+
type Tree = BinarySearch;
7276
type Value = i128;
7377

7478
fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {

tooling/fuzzer/src/strategies/mod.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,28 @@ use uint::UintStrategy;
1111
mod int;
1212
mod uint;
1313

14+
/// Create a strategy for generating random values for an [AbiType].
15+
///
16+
/// Uses the `dictionary` for unsigned integer types.
1417
pub(super) fn arb_value_from_abi_type(
1518
abi_type: &AbiType,
16-
dictionary: HashSet<FieldElement>,
19+
dictionary: &HashSet<FieldElement>,
1720
) -> SBoxedStrategy<InputValue> {
1821
match abi_type {
1922
AbiType::Field => vec(any::<u8>(), 32)
2023
.prop_map(|bytes| InputValue::Field(FieldElement::from_be_bytes_reduce(&bytes)))
2124
.sboxed(),
2225
AbiType::Integer { width, sign } if sign == &Sign::Unsigned => {
23-
UintStrategy::new(*width as usize, dictionary)
26+
// We've restricted the type system to only allow u64s as the maximum integer type.
27+
let width = (*width).min(64);
28+
UintStrategy::new(width as usize, dictionary)
2429
.prop_map(|uint| InputValue::Field(uint.into()))
2530
.sboxed()
2631
}
2732
AbiType::Integer { width, .. } => {
28-
let shift = 2i128.pow(*width);
29-
IntStrategy::new(*width as usize)
33+
let width = (*width).min(64);
34+
let shift = 2i128.pow(width);
35+
IntStrategy::new(width as usize)
3036
.prop_map(move |mut int| {
3137
if int < 0 {
3238
int += shift
@@ -38,7 +44,6 @@ pub(super) fn arb_value_from_abi_type(
3844
AbiType::Boolean => {
3945
any::<bool>().prop_map(|val| InputValue::Field(FieldElement::from(val))).sboxed()
4046
}
41-
4247
AbiType::String { length } => {
4348
// Strings only allow ASCII characters as each character must be able to be represented by a single byte.
4449
let string_regex = format!("[[:ascii:]]{{{length}}}");
@@ -53,12 +58,11 @@ pub(super) fn arb_value_from_abi_type(
5358

5459
elements.prop_map(InputValue::Vec).sboxed()
5560
}
56-
5761
AbiType::Struct { fields, .. } => {
5862
let fields: Vec<SBoxedStrategy<(String, InputValue)>> = fields
5963
.iter()
6064
.map(|(name, typ)| {
61-
(Just(name.clone()), arb_value_from_abi_type(typ, dictionary.clone())).sboxed()
65+
(Just(name.clone()), arb_value_from_abi_type(typ, dictionary)).sboxed()
6266
})
6367
.collect();
6468

@@ -69,25 +73,25 @@ pub(super) fn arb_value_from_abi_type(
6973
})
7074
.sboxed()
7175
}
72-
7376
AbiType::Tuple { fields } => {
7477
let fields: Vec<_> =
75-
fields.iter().map(|typ| arb_value_from_abi_type(typ, dictionary.clone())).collect();
78+
fields.iter().map(|typ| arb_value_from_abi_type(typ, dictionary)).collect();
7679
fields.prop_map(InputValue::Vec).sboxed()
7780
}
7881
}
7982
}
8083

84+
/// Given the [Abi] description of a [ProgramArtifact], generate random [InputValue]s for each circuit parameter.
85+
///
86+
/// Use the `dictionary` to draw values from for numeric types.
8187
pub(super) fn arb_input_map(
8288
abi: &Abi,
83-
dictionary: HashSet<FieldElement>,
89+
dictionary: &HashSet<FieldElement>,
8490
) -> BoxedStrategy<InputMap> {
8591
let values: Vec<_> = abi
8692
.parameters
8793
.iter()
88-
.map(|param| {
89-
(Just(param.name.clone()), arb_value_from_abi_type(&param.typ, dictionary.clone()))
90-
})
94+
.map(|param| (Just(param.name.clone()), arb_value_from_abi_type(&param.typ, dictionary)))
9195
.collect();
9296

9397
values

tooling/fuzzer/src/strategies/uint.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ use proptest::{
77
};
88
use rand::Rng;
99

10+
type BinarySearch = proptest::num::u128::BinarySearch;
11+
1012
/// Value tree for unsigned ints (up to u128).
1113
/// The strategy combines 2 different strategies, each assigned a specific weight:
1214
/// 1. Generate purely random value in a range. This will first choose bit size uniformly (up `bits`
1315
/// param). Then generate a value for this bit size.
1416
/// 2. Generate a random value around the edges (+/- 3 around 0 and max possible value)
1517
#[derive(Debug)]
1618
pub struct UintStrategy {
17-
/// Bit size of uint (e.g. 128)
19+
/// Bit size of uint (e.g. 64)
1820
bits: usize,
1921
/// A set of fixtures to be generated
2022
fixtures: Vec<FieldElement>,
@@ -31,25 +33,29 @@ impl UintStrategy {
3133
/// # Arguments
3234
/// * `bits` - Size of uint in bits
3335
/// * `fixtures` - Set of `FieldElements` representing values which the fuzzer weight towards testing.
34-
pub fn new(bits: usize, fixtures: HashSet<FieldElement>) -> Self {
36+
pub fn new(bits: usize, fixtures: &HashSet<FieldElement>) -> Self {
3537
Self {
3638
bits,
37-
fixtures: fixtures.into_iter().collect(),
39+
// We can only consider the fixtures which fit into the bit width.
40+
fixtures: fixtures.iter().filter(|f| f.num_bits() <= bits as u32).copied().collect(),
3841
edge_weight: 10usize,
3942
fixtures_weight: 40usize,
4043
random_weight: 50usize,
4144
}
4245
}
4346

47+
/// Generate random numbers starting from near 0 or the maximum of the range.
4448
fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
4549
let rng = runner.rng();
4650
// Choose if we want values around 0 or max
4751
let is_min = rng.gen_bool(0.5);
4852
let offset = rng.gen_range(0..4);
4953
let start = if is_min { offset } else { self.type_max().saturating_sub(offset) };
50-
Ok(proptest::num::u128::BinarySearch::new(start))
54+
Ok(BinarySearch::new(start))
5155
}
5256

57+
/// Pick a random `FieldElement` from the `fixtures` as a starting point for
58+
/// generating random numbers.
5359
fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
5460
// generate random cases if there's no fixtures
5561
if self.fixtures.is_empty() {
@@ -58,21 +64,19 @@ impl UintStrategy {
5864

5965
// Generate value tree from fixture.
6066
let fixture = &self.fixtures[runner.rng().gen_range(0..self.fixtures.len())];
61-
if fixture.num_bits() <= self.bits as u32 {
62-
return Ok(proptest::num::u128::BinarySearch::new(fixture.to_u128()));
63-
}
6467

65-
// If fixture is not a valid type, generate random value.
66-
self.generate_random_tree(runner)
68+
Ok(BinarySearch::new(fixture.to_u128()))
6769
}
6870

71+
/// Generate random values between 0 and the MAX with the given bit width.
6972
fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
7073
let rng = runner.rng();
7174
let start = rng.gen_range(0..=self.type_max());
7275

73-
Ok(proptest::num::u128::BinarySearch::new(start))
76+
Ok(BinarySearch::new(start))
7477
}
7578

79+
/// Maximum integer that fits in the given bit width.
7680
fn type_max(&self) -> u128 {
7781
if self.bits < 128 {
7882
(1 << self.bits) - 1
@@ -83,8 +87,10 @@ impl UintStrategy {
8387
}
8488

8589
impl Strategy for UintStrategy {
86-
type Tree = proptest::num::u128::BinarySearch;
90+
type Tree = BinarySearch;
8791
type Value = u128;
92+
93+
/// Pick randomly from the 3 available strategies for generating unsigned integers.
8894
fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
8995
let total_weight = self.random_weight + self.fixtures_weight + self.edge_weight;
9096
let bias = runner.rng().gen_range(0..total_weight);

0 commit comments

Comments
 (0)