use super::*;
use crate::adapter::fuzz_common::{run_structural_fuzz, FuzzOutcome};
use arbitrary::{Arbitrary, Unstructured};
const FUZZ_MAX_DEPTH: u32 = 2;
#[test]
fn test_adapter_async_5_u32_params_validates() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let iface = make_iface(vec![(
"many",
sig(
true,
&["a", "b", "c", "d", "e"],
vec![u32_id; 5], vec![u32_id],
),
)]);
let bytes = gen_adapter(
"test:pkg/many@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_mixed_primitives_indirect_params_validates() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let u64_id = arena.intern_val(ValueType::U64);
let f32_id = arena.intern_val(ValueType::F32);
let f64_id = arena.intern_val(ValueType::F64);
let bool_id = arena.intern_val(ValueType::Bool);
let char_id = arena.intern_val(ValueType::Char);
let iface = make_iface(vec![(
"mixed",
sig(
true,
&["a", "b", "c", "d", "e", "f"],
vec![u32_id, u64_id, f32_id, f64_id, bool_id, char_id], vec![u32_id],
),
)]);
let bytes = gen_adapter(
"test:pkg/mixed-async@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_record_param_indirect_params_validates() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let record = arena.intern_val(ValueType::Record(vec![
("a".into(), u32_id),
("b".into(), u32_id),
("c".into(), u32_id),
("d".into(), u32_id),
("e".into(), u32_id),
]));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([(
"many".to_string(),
sig(true, &["r"], vec![record], vec![u32_id]),
)]),
type_exports: BTreeMap::from([("rec5".to_string(), record)]),
});
let bytes = gen_adapter(
"test:pkg/rec5-async@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_tuple_param_indirect_params_validates() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let u64_id = arena.intern_val(ValueType::U64);
let f32_id = arena.intern_val(ValueType::F32);
let f64_id = arena.intern_val(ValueType::F64);
let bool_id = arena.intern_val(ValueType::Bool);
let tup = arena.intern_val(ValueType::Tuple(vec![
u32_id, u64_id, f32_id, f64_id, bool_id,
]));
let iface = make_iface(vec![("many", sig(true, &["t"], vec![tup], vec![u32_id]))]);
let bytes = gen_adapter(
"test:pkg/tup5-async@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_enum_flags_record_indirect_params_validates() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let color = arena.intern_val(ValueType::Enum(vec![
"red".into(),
"green".into(),
"blue".into(),
]));
let perms = arena.intern_val(ValueType::Flags(vec![
"read".into(),
"write".into(),
"exec".into(),
]));
let record = arena.intern_val(ValueType::Record(vec![
("c".into(), color),
("f".into(), perms),
("a".into(), u32_id),
("b".into(), u32_id),
("d".into(), u32_id),
]));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([(
"many".to_string(),
sig(true, &["r"], vec![record], vec![u32_id]),
)]),
type_exports: BTreeMap::from([
("color".to_string(), color),
("perms".to_string(), perms),
("rec5".to_string(), record),
]),
});
let bytes = gen_adapter(
"test:pkg/cfr-async@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_string_list_indirect_params_validates() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let string_id = arena.intern_val(ValueType::String);
let list_u32 = arena.intern_val(ValueType::List(u32_id));
let iface = make_iface(vec![(
"many",
sig(
true,
&["s", "l", "a", "b", "c"],
vec![string_id, list_u32, u32_id, u32_id, u32_id],
vec![u32_id],
),
)]);
let bytes = gen_adapter(
"test:pkg/strlst-async@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_fixed_list_indirect_params_validates() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let fsl = arena.intern_val(ValueType::FixedSizeList(u32_id, 4));
let iface = make_iface(vec![(
"many",
sig(
true,
&["fl", "a", "b"],
vec![fsl, u32_id, u32_id],
vec![u32_id],
),
)]);
let bytes = gen_adapter(
"test:pkg/fl-async@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_async_variant_option_result_indirect_params_validates() {
let mut arena = TypeArena::default();
let u32_id = arena.intern_val(ValueType::U32);
let u64_id = arena.intern_val(ValueType::U64);
let opt_u32 = arena.intern_val(ValueType::Option(u32_id));
let res = arena.intern_val(ValueType::Result {
ok: Some(u32_id),
err: Some(u64_id),
});
let either = arena.intern_val(ValueType::Variant(vec![
("left".into(), Some(u32_id)),
("right".into(), Some(u64_id)),
("neither".into(), None),
]));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([(
"many".to_string(),
sig(
true,
&["o", "r", "v", "a", "b"],
vec![opt_u32, res, either, u32_id, u32_id],
vec![u32_id],
),
)]),
type_exports: BTreeMap::from([("either".to_string(), either)]),
});
let bytes = gen_adapter(
"test:pkg/disp-async@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
#[test]
fn test_adapter_record_with_list_field_repro() {
let mut arena = TypeArena::default();
let char_id = arena.intern_val(ValueType::Char);
let list_id = arena.intern_val(ValueType::List(char_id));
let record_id = arena.intern_val(ValueType::Record(vec![("f0".into(), list_id)]));
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([("get".to_string(), sig(true, &[], vec![], vec![record_id]))]),
type_exports: BTreeMap::from([("rec".to_string(), record_id)]),
});
let bytes = gen_adapter(
"test:repro/rec@1.0.0",
&["splicer:tier1/before", "splicer:tier1/after"],
&iface,
&arena,
SplitKind::Consumer,
);
validate_component(&bytes);
}
fn fuzz_primitive(u: &mut Unstructured<'_>) -> arbitrary::Result<ValueType> {
let ctors: &[fn() -> ValueType] = &[
|| ValueType::Bool,
|| ValueType::S8,
|| ValueType::U8,
|| ValueType::S16,
|| ValueType::U16,
|| ValueType::S32,
|| ValueType::U32,
|| ValueType::S64,
|| ValueType::U64,
|| ValueType::F32,
|| ValueType::F64,
|| ValueType::Char,
|| ValueType::String,
];
Ok(ctors[u.choose_index(ctors.len())?]())
}
fn fuzz_value_type(
u: &mut Unstructured<'_>,
arena: &mut TypeArena,
depth: u32,
need_export: &mut Vec<ValueTypeId>,
) -> arbitrary::Result<ValueTypeId> {
if depth == 0 {
return Ok(arena.intern_val(fuzz_primitive(u)?));
}
match u.choose_index(11)? {
0 => Ok(arena.intern_val(fuzz_primitive(u)?)),
1 => {
let inner = fuzz_value_type(u, arena, depth - 1, need_export)?;
Ok(arena.intern_val(ValueType::List(inner)))
}
2 => {
let inner = fuzz_value_type(u, arena, depth - 1, need_export)?;
let n = u.int_in_range::<u32>(1..=8)?;
Ok(arena.intern_val(ValueType::FixedSizeList(inner, n)))
}
3 => {
let count = u.int_in_range(2..=4)?;
let mut ids = Vec::with_capacity(count);
for _ in 0..count {
ids.push(fuzz_value_type(u, arena, depth - 1, need_export)?);
}
Ok(arena.intern_val(ValueType::Tuple(ids)))
}
4 => {
let inner = fuzz_value_type(u, arena, depth - 1, need_export)?;
Ok(arena.intern_val(ValueType::Option(inner)))
}
5 => {
let ok = if bool::arbitrary(u)? {
Some(fuzz_value_type(u, arena, depth - 1, need_export)?)
} else {
None
};
let err = if bool::arbitrary(u)? {
Some(fuzz_value_type(u, arena, depth - 1, need_export)?)
} else {
None
};
Ok(arena.intern_val(ValueType::Result { ok, err }))
}
6 => {
let count = u.int_in_range(1..=4)?;
let mut fields = Vec::with_capacity(count);
for i in 0..count {
let fid = fuzz_value_type(u, arena, depth - 1, need_export)?;
fields.push((format!("f{i}"), fid));
}
let id = arena.intern_val(ValueType::Record(fields));
need_export.push(id);
Ok(id)
}
7 => {
let count = u.int_in_range(1..=4)?;
let mut cases = Vec::with_capacity(count);
for i in 0..count {
let payload = if bool::arbitrary(u)? {
Some(fuzz_value_type(u, arena, depth - 1, need_export)?)
} else {
None
};
cases.push((format!("c{i}"), payload));
}
let id = arena.intern_val(ValueType::Variant(cases));
need_export.push(id);
Ok(id)
}
8 => {
let count = u.int_in_range(1..=4)?;
let tags: Vec<String> = (0..count).map(|i| format!("t{i}")).collect();
let id = arena.intern_val(ValueType::Enum(tags));
need_export.push(id);
Ok(id)
}
9 => {
let count = u.int_in_range::<usize>(1..=32)?;
let labels: Vec<String> = (0..count).map(|i| format!("fl{i}")).collect();
let id = arena.intern_val(ValueType::Flags(labels));
need_export.push(id);
Ok(id)
}
_ => Ok(arena.intern_val(fuzz_primitive(u)?)),
}
}
fn fuzz_is_expected_bail(msg: &str) -> bool {
msg.contains("flat parameter values")
|| msg.contains("flat representation")
|| msg.contains("exceeds 16") || msg.contains("results; only 0 or 1 results")
|| msg.contains("not yet implemented")
}
#[test]
fn fuzz_structural_shapes() {
run_structural_fuzz("tier1-fuzz", |bytes| {
let mut u = Unstructured::new(bytes);
let mut arena = TypeArena::default();
let mut need_export: Vec<ValueTypeId> = Vec::new();
let result_id = fuzz_value_type(&mut u, &mut arena, FUZZ_MAX_DEPTH, &mut need_export)
.map_err(|_| "ran out of random bytes".to_string())?;
let shape = arena.canonical_val(result_id);
let type_exports: BTreeMap<String, ValueTypeId> = need_export
.iter()
.enumerate()
.map(|(idx, id)| (format!("ty{idx}"), *id))
.collect();
let iface = InterfaceType::Instance(InstanceInterface {
functions: BTreeMap::from([(
"get".to_string(),
sig(true, &[], vec![], vec![result_id]),
)]),
type_exports,
});
let tmp = tempfile::tempdir().unwrap();
let hooks = [
"splicer:tier1/before".to_string(),
"splicer:tier1/after".to_string(),
];
let split = synth_split("test:fuzz/iface@1.0.0", &iface, &arena, SplitKind::Consumer);
let split_path = split.path().to_str().unwrap();
let gen = crate::adapter::generate_tier1_adapter(
"fuzz-mdl",
"test:fuzz/iface@1.0.0",
&hooks,
tmp.path().to_str().unwrap(),
split_path,
);
match gen {
Ok(path) => {
let bytes = std::fs::read(&path).map_err(|e| format!("read: {e}"))?;
let mut validator =
wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
validator
.validate_all(&bytes)
.map_err(|e| format!("invalid component for shape `{shape}`: {e}"))?;
Ok(FuzzOutcome::Passed)
}
Err(e) => {
let msg = format!("{e:#}");
if fuzz_is_expected_bail(&msg) {
Ok(FuzzOutcome::ExpectedBail)
} else {
Err(format!("unexpected bail for shape `{shape}`: {msg}"))
}
}
}
});
}