use proptest::prelude::*;
use serde_json::Value as J;
use tono_core::dsl::{Adsr, SeqWave};
use tono_core::dsp::Rng;
use tono_core::program::Program;
use tono_core::song::{CompileOptions, Marker, Section, Song, note};
use tono_core::units::Beat;
fn config() -> ProptestConfig {
ProptestConfig {
cases: 128,
failure_persistence: None,
..ProptestConfig::default()
}
}
fn base_bundle() -> &'static str {
static BASE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
BASE.get_or_init(|| {
let amp = Adsr {
a: 0.005,
d: 0.1,
s: 0.8,
r: 0.2,
punch: 0.0,
};
let mut song = Song::new("fuzz-bundle", 120.0);
song.add_track("bass", SeqWave::Bass, amp);
song.add_track("keys", SeqWave::Epiano, amp);
song.add_pattern("riff", 1, vec![note(0, 4, "C2"), note(8, 4, "G2")]);
song.add_pattern("stab", 1, vec![note(0, 2, "C4"), note(6, 2, "D#4")]);
song.arrange_repeat("bass", "riff", 0, 2);
song.arrange_repeat("keys", "stab", 0, 2);
song.sections.push(Section {
name: "second".into(),
bar: 1,
bars: 1,
});
song.markers.push(Marker {
name: "hit".into(),
at: Beat::from_int(2),
});
song.compile(&CompileOptions::default())
.expect("the base song compiles")
.to_json()
})
}
fn random_scalar(rng: &mut Rng) -> J {
match rng.next_u64() % 16 {
0 => J::Null,
1 => J::Bool(true),
2 => J::Bool(false),
3 => J::from(0u64),
4 => J::from(1u64),
5 => J::from(-1i64),
6 => J::from(u32::MAX),
7 => J::from(u64::MAX),
8 => J::from(i64::MIN),
9 => J::from(0.5f64),
10 => J::from(1e308_f64),
11 => J::from(-1e308_f64),
12 => J::from(""),
13 => J::from("midi:36"),
14 => J::Array(vec![]),
_ => J::Object(serde_json::Map::new()),
}
}
fn scramble(value: &mut J, rng: &mut Rng, budget: &mut u32) {
let take_leaf = |rng: &mut Rng| rng.next_u64().is_multiple_of(3);
match value {
J::Object(map) => {
for val in map.values_mut() {
if *budget == 0 {
return;
}
if take_leaf(rng) {
*val = random_scalar(rng);
*budget -= 1;
} else {
scramble(val, rng, budget);
}
}
}
J::Array(items) => {
for val in items.iter_mut() {
if *budget == 0 {
return;
}
if take_leaf(rng) {
*val = random_scalar(rng);
*budget -= 1;
} else {
scramble(val, rng, budget);
}
}
}
_ => {
*value = random_scalar(rng);
*budget -= 1;
}
}
}
const DUP_KEYS: &[&str] = &[
"program_version",
"schema_version",
"engine_version",
"hash",
"target",
"doc",
"meta",
];
fn mutated_bundle() -> BoxedStrategy<String> {
(
any::<u64>(),
0..=6u32,
proptest::option::of((proptest::sample::select(DUP_KEYS), any::<u64>())),
proptest::option::of(0.0f64..1.0),
)
.prop_map(|(seed, budget, dup, trunc)| {
let mut text = base_bundle().to_string();
if budget > 0 {
let mut value: J = serde_json::from_str(&text).expect("the base bundle parses");
let mut rng = Rng::new(seed);
let mut budget = budget;
scramble(&mut value, &mut rng, &mut budget);
text = serde_json::to_string(&value).expect("a scrambled value serializes");
}
if let Some((key, raw)) = dup {
text = format!("{{\"{key}\":{raw},{}", &text[1..]);
}
if let Some(frac) = trunc {
let keep = (frac * text.chars().count() as f64) as usize;
text = text.chars().take(keep).collect();
}
text
})
.boxed()
}
fn assert_load_contract(text: &str) -> Result<(), TestCaseError> {
match Program::from_json(text) {
Ok(program) => {
let json = program.to_json();
let reloaded = Program::from_json(&json)
.expect("a bundle that loaded once must load from its own to_json");
prop_assert_eq!(reloaded.hash, program.hash, "hash unstable on reload");
prop_assert_eq!(
reloaded.to_json(),
json,
"to_json is not a fixpoint for a loaded bundle"
);
}
Err(err) => {
let _ = err.to_string();
}
}
Ok(())
}
proptest! {
#![proptest_config(config())]
#[test]
fn from_json_never_panics_on_arbitrary_input(
bytes in proptest::collection::vec(any::<u8>(), 0..=300),
text in any::<String>(),
) {
assert_load_contract(&String::from_utf8_lossy(&bytes))?;
assert_load_contract(&text)?;
}
#[test]
fn mutated_bundles_load_typed_or_round_trip(text in mutated_bundle()) {
assert_load_contract(&text)?;
}
#[test]
fn torn_or_hand_edited_bundles_behave(
dup in proptest::option::of(proptest::sample::select(DUP_KEYS)),
trunc in proptest::option::of(0.0f64..1.0),
) {
let mut text = base_bundle().to_string();
if let Some(key) = dup {
text = format!("{{\"{key}\":0,{}", &text[1..]);
}
if let Some(frac) = trunc {
let keep = (frac * text.chars().count() as f64) as usize;
text = text.chars().take(keep).collect();
}
assert_load_contract(&text)?;
}
}