use axon_frontend::ast::{Declaration, Program};
use axon_frontend::lexer::Lexer;
use axon_frontend::parser::Parser;
use axon_frontend::type_checker::{
compute_implicit_transport_warnings, compute_implicit_transports,
};
#[derive(Clone, Copy)]
struct Xorshift(u64);
impl Xorshift {
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn next_usize(&mut self, max: usize) -> usize {
if max == 0 {
0
} else {
(self.next_u64() as usize) % max
}
}
}
const SEED_PROGRAMS: &[&str] = &[
"tool t { description: \"t\" effects: <stream:drop_oldest> }\n\
flow F() -> Unit { step S { ask: \"x\" apply: t } }\n\
axonendpoint E { method: POST path: \"/f\" execute: F }",
"tool t { description: \"t\" effects: <stream:drop_oldest> }\n\
flow F() -> Unit { step S { ask: \"x\" apply: t } }\n\
axonendpoint E { method: POST path: \"/f\" execute: F transport: json }",
"tool t { description: \"t\" effects: <stream:drop_oldest> }\n\
flow F() -> Unit { step S { ask: \"x\" apply: t } }\n\
axonendpoint E { method: POST path: \"/f\" execute: F transport: sse }",
"flow F() -> Int { step S { ask: \"x\" output: Int } }\n\
axonendpoint E { method: POST path: \"/f\" execute: F }",
"tool t { description: \"t\" effects: <network> }\n\
flow F() -> Unit { step S { ask: \"x\" apply: t } }\n\
axonendpoint E { method: POST path: \"/f\" execute: F }",
];
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\
0123456789 :{}()[]<>,\"\n_";
fn mutate(source: &[u8], rng: &mut Xorshift) -> Vec<u8> {
let mut bytes = source.to_vec();
let n_mut = rng.next_usize(4); for _ in 0..n_mut {
if bytes.is_empty() {
break;
}
let op = rng.next_usize(4);
let i = rng.next_usize(bytes.len());
match op {
0 => {
bytes.remove(i);
}
1 => {
let b = ALPHABET[rng.next_usize(ALPHABET.len())];
bytes.insert(i, b);
}
2 if i + 1 < bytes.len() => {
bytes.swap(i, i + 1);
}
_ => {
let b = ALPHABET[rng.next_usize(ALPHABET.len())];
bytes[i] = b;
}
}
}
bytes.retain(|b| b.is_ascii());
bytes
}
fn try_parse(src: &str) -> Option<Program> {
let tokens = Lexer::new(src, "<fuzz>").tokenize().ok()?;
Parser::new(tokens).parse().ok()
}
#[test]
fn inference_pass_never_panics_under_byte_fuzz() {
let mut total_iter = 0usize;
let mut exercised = 0usize;
for bucket in 0..100u64 {
let mut rng = Xorshift(0x3131_aaff_dead_beef_u64.wrapping_add(bucket));
for _iter in 0..10 {
total_iter += 1;
let base = SEED_PROGRAMS[rng.next_usize(SEED_PROGRAMS.len())];
let mutated_bytes = mutate(base.as_bytes(), &mut rng);
let mutated = match std::str::from_utf8(&mutated_bytes) {
Ok(s) => s.to_string(),
Err(_) => continue,
};
let mut program = match try_parse(&mutated) {
Some(p) => p,
None => continue, };
exercised += 1;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compute_implicit_transports(&mut program);
}));
assert!(
result.is_ok(),
"compute_implicit_transports panicked (bucket={bucket}, source={mutated:?})"
);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = compute_implicit_transport_warnings(&program);
}));
assert!(
result.is_ok(),
"compute_implicit_transport_warnings panicked (bucket={bucket}, source={mutated:?})"
);
}
}
assert!(
exercised >= 100,
"expected ≥ 100 of {total_iter} fuzz iterations to reach the inference pass, got {exercised}"
);
}
#[test]
fn implicit_transport_always_in_closed_set_under_fuzz() {
for bucket in 0..100u64 {
let mut rng = Xorshift(0x3131_c105_ed5e_7000_u64.wrapping_add(bucket));
for _iter in 0..10 {
let base = SEED_PROGRAMS[rng.next_usize(SEED_PROGRAMS.len())];
let mutated_bytes = mutate(base.as_bytes(), &mut rng);
let mutated = match std::str::from_utf8(&mutated_bytes) {
Ok(s) => s.to_string(),
Err(_) => continue,
};
let mut program = match try_parse(&mutated) {
Some(p) => p,
None => continue,
};
compute_implicit_transports(&mut program);
for decl in &program.declarations {
if let Declaration::AxonEndpoint(ae) = decl {
assert!(
ae.implicit_transport == "sse" || ae.implicit_transport == "json",
"implicit_transport drifted from closed set (bucket={bucket}): \
got {:?}, source={mutated:?}",
ae.implicit_transport
);
}
}
}
}
}
#[test]
fn idempotence_holds_under_fuzz() {
for bucket in 0..25u64 {
let mut rng = Xorshift(0x3131_1de0_e07e_d120_u64.wrapping_add(bucket));
for _iter in 0..10 {
let base = SEED_PROGRAMS[rng.next_usize(SEED_PROGRAMS.len())];
let mutated_bytes = mutate(base.as_bytes(), &mut rng);
let mutated = match std::str::from_utf8(&mutated_bytes) {
Ok(s) => s.to_string(),
Err(_) => continue,
};
let mut program = match try_parse(&mutated) {
Some(p) => p,
None => continue,
};
compute_implicit_transports(&mut program);
let first: Vec<(String, String)> = program
.declarations
.iter()
.filter_map(|d| match d {
Declaration::AxonEndpoint(ae) => {
Some((ae.name.clone(), ae.implicit_transport.clone()))
}
_ => None,
})
.collect();
compute_implicit_transports(&mut program);
let second: Vec<(String, String)> = program
.declarations
.iter()
.filter_map(|d| match d {
Declaration::AxonEndpoint(ae) => {
Some((ae.name.clone(), ae.implicit_transport.clone()))
}
_ => None,
})
.collect();
assert_eq!(
first, second,
"idempotence violated (bucket={bucket}, source={mutated:?})"
);
}
}
}