use std::collections::BTreeSet;
use crate::fixtures::graph_of;
use sva_ast::Graph;
use sva_engine::{Cache, CacheStats, Hash, Outcome, RenderConfig, render};
const SECONDS: f64 = 0.05;
const RATE: u32 = 8_000;
fn demo(name: &str) -> Graph {
graph_of(
name,
&[
("osc", "saw(110*t) + 0.5*square(165*t)\n"),
("pad", "lowpass(sample(@osc), cutoff=900, q=0.8)\n"),
("a", "@pad*0.8\n"),
("b", "@pad*0.5 + sample(sin(2*pi*440*t))*0.2\n"),
("c", "tanh(@pad*2)*0.5\n"),
],
)
}
fn stats(graph: &Graph, root: &str, cache: &Cache) -> CacheStats {
render(
graph,
root,
RenderConfig::seconds(RATE, SECONDS),
Some(cache),
)
.unwrap_or_else(|e| panic!("rendering `{root}`: {e}"))
.cache_stats
.expect("a render handed a store reports on it")
}
fn keys(stats: &CacheStats) -> BTreeSet<(Hash, String)> {
stats
.lookups
.iter()
.map(|l| (l.key, format!("{}:{:?}", l.node, l.kind)))
.collect()
}
#[test]
fn a_render_handed_no_store_reports_its_own_reuse_and_no_store() {
let graph = demo("no-store");
let held = render(&graph, "b", RenderConfig::seconds(RATE, SECONDS), None).expect("a render");
let stats = held
.cache_stats
.expect("every render reports what it asked");
assert_eq!((stats.bytes, stats.entries), (0, 0));
assert!(stats.computed() > 0 && stats.stored() == 0, "{stats:?}");
let pad: Vec<_> = stats.lookups.iter().filter(|l| l.node == "pad").collect();
assert_eq!(pad.len(), 1, "one read of the pad: {stats:?}");
}
#[test]
fn an_identical_second_render_is_all_hits() {
let graph = demo("identical");
let cache = Cache::new();
let cold = stats(&graph, "b", &cache);
let warm = stats(&graph, "b", &cache);
assert!(!warm.lookups.is_empty());
assert!(
keys(&warm).is_subset(&keys(&cold)),
"lookups the first render asked"
);
assert_eq!(warm.hits(), warm.lookups.len());
assert_eq!(warm.computed(), 0);
}
#[test]
fn a_reuse_is_noted_where_its_read_first_sounds() {
let graph = graph_of(
"reuse-placed",
&[
("note", "crop(sin(2*pi*440*t)*exp(-t/0.2), 0s, 0.5s)\n"),
("song", "@note(t) + @note(t - 2s) + @note(t - 4s)\n"),
],
);
let held = render(&graph, "song", RenderConfig::seconds(RATE, 6.0), None).expect("a render");
let stats = held
.cache_stats
.expect("every render reports what it asked");
let made_by = |secs: f64| {
let at = (secs * f64::from(RATE)) as i64;
stats
.reached
.iter()
.take_while(|(reached, _)| *reached <= at)
.last()
.map_or(0, |(_, made)| *made)
};
let reuses: Vec<usize> = (0..stats.lookups.len())
.filter(|&i| stats.lookups[i].node == "note" && stats.lookups[i].outcome == Outcome::Hit)
.collect();
assert_eq!(reuses.len(), 2, "{stats:?}");
for (at, secs) in reuses.into_iter().zip([2.0, 4.0]) {
assert!(
made_by(secs - 0.5) <= at && at < made_by(secs + 0.5),
"the read at {secs} s was noted as lookup {at}: {:?}",
stats.reached
);
}
}
#[test]
fn a_read_through_a_moved_node_is_a_lookup_of_the_node_it_moves() {
let graph = graph_of(
"reuse-moved",
&[
("a", "crop(sin(2*pi*440*t)*exp(-t/0.1), 0s, 0.5s)\n"),
("b", "@a(t - 1s)\n"),
("song", "@b(t) + @b(t - 2s)\n"),
],
);
let held = render(&graph, "song", RenderConfig::seconds(RATE, 4.0), None).expect("a render");
let stats = held
.cache_stats
.expect("every render reports what it asked");
let of = |node: &str| {
stats
.lookups
.iter()
.filter(|l| l.node == node)
.map(|l| l.outcome)
.collect::<Vec<_>>()
};
assert_eq!(
of("a"),
[Outcome::ComputedNotStored, Outcome::Hit],
"{stats:?}"
);
}