use std::panic::UnwindSafe;
use expect_test::expect;
use salsa::{Durability, ParallelDatabase, Snapshot};
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
struct Error {
cycle: Vec<String>,
}
#[salsa::database(GroupStruct)]
#[derive(Default)]
struct DatabaseImpl {
storage: salsa::Storage<Self>,
}
impl salsa::Database for DatabaseImpl {}
impl ParallelDatabase for DatabaseImpl {
fn snapshot(&self) -> Snapshot<Self> {
Snapshot::new(DatabaseImpl { storage: self.storage.snapshot() })
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum CycleQuery {
None,
A,
B,
C,
AthenC,
}
#[salsa::query_group(GroupStruct)]
trait Database: salsa::Database {
fn memoized_a(&self) -> ();
fn memoized_b(&self) -> ();
fn volatile_a(&self) -> ();
fn volatile_b(&self) -> ();
#[salsa::input]
fn a_invokes(&self) -> CycleQuery;
#[salsa::input]
fn b_invokes(&self) -> CycleQuery;
#[salsa::input]
fn c_invokes(&self) -> CycleQuery;
#[salsa::cycle(recover_a)]
fn cycle_a(&self) -> Result<(), Error>;
#[salsa::cycle(recover_b)]
fn cycle_b(&self) -> Result<(), Error>;
fn cycle_c(&self) -> Result<(), Error>;
}
fn recover_a(db: &dyn Database, cycle: &salsa::Cycle) -> Result<(), Error> {
Err(Error { cycle: cycle.all_participants(db) })
}
fn recover_b(db: &dyn Database, cycle: &salsa::Cycle) -> Result<(), Error> {
Err(Error { cycle: cycle.all_participants(db) })
}
fn memoized_a(db: &dyn Database) {
db.memoized_b()
}
fn memoized_b(db: &dyn Database) {
db.memoized_a()
}
fn volatile_a(db: &dyn Database) {
db.salsa_runtime().report_untracked_read();
db.volatile_b()
}
fn volatile_b(db: &dyn Database) {
db.salsa_runtime().report_untracked_read();
db.volatile_a()
}
impl CycleQuery {
fn invoke(self, db: &dyn Database) -> Result<(), Error> {
match self {
CycleQuery::A => db.cycle_a(),
CycleQuery::B => db.cycle_b(),
CycleQuery::C => db.cycle_c(),
CycleQuery::AthenC => {
let _ = db.cycle_a();
db.cycle_c()
}
CycleQuery::None => Ok(()),
}
}
}
fn cycle_a(db: &dyn Database) -> Result<(), Error> {
db.a_invokes().invoke(db)
}
fn cycle_b(db: &dyn Database) -> Result<(), Error> {
db.b_invokes().invoke(db)
}
fn cycle_c(db: &dyn Database) -> Result<(), Error> {
db.c_invokes().invoke(db)
}
#[track_caller]
fn extract_cycle(f: impl FnOnce() + UnwindSafe) -> salsa::Cycle {
let v = std::panic::catch_unwind(f);
if let Err(d) = &v {
if let Some(cycle) = d.downcast_ref::<salsa::Cycle>() {
return cycle.clone();
}
}
panic!("unexpected value: {:?}", v)
}
#[test]
fn cycle_memoized() {
let db = DatabaseImpl::default();
let cycle = extract_cycle(|| db.memoized_a());
expect![[r#"
[
"cycles::MemoizedAQuery::memoized_a(())",
"cycles::MemoizedBQuery::memoized_b(())",
]
"#]]
.assert_debug_eq(&cycle.unexpected_participants(&db));
}
#[test]
fn cycle_volatile() {
let db = DatabaseImpl::default();
let cycle = extract_cycle(|| db.volatile_a());
expect![[r#"
[
"cycles::VolatileAQuery::volatile_a(())",
"cycles::VolatileBQuery::volatile_b(())",
]
"#]]
.assert_debug_eq(&cycle.unexpected_participants(&db));
}
#[test]
fn cycle_cycle() {
let mut query = DatabaseImpl::default();
query.set_a_invokes(CycleQuery::B);
query.set_b_invokes(CycleQuery::A);
assert!(query.cycle_a().is_err());
}
#[test]
fn inner_cycle() {
let mut query = DatabaseImpl::default();
query.set_a_invokes(CycleQuery::B);
query.set_b_invokes(CycleQuery::A);
query.set_c_invokes(CycleQuery::B);
let err = query.cycle_c();
assert!(err.is_err());
let cycle = err.unwrap_err().cycle;
expect![[r#"
[
"cycles::CycleAQuery::cycle_a(())",
"cycles::CycleBQuery::cycle_b(())",
]
"#]]
.assert_debug_eq(&cycle);
}
#[test]
fn cycle_revalidate() {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::B);
db.set_b_invokes(CycleQuery::A);
assert!(db.cycle_a().is_err());
db.set_b_invokes(CycleQuery::A); assert!(db.cycle_a().is_err());
}
#[test]
fn cycle_revalidate_unchanged_twice() {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::B);
db.set_b_invokes(CycleQuery::A);
assert!(db.cycle_a().is_err());
db.set_c_invokes(CycleQuery::A);
expect![[r#"
Err(
Error {
cycle: [
"cycles::CycleAQuery::cycle_a(())",
"cycles::CycleBQuery::cycle_b(())",
],
},
)
"#]]
.assert_debug_eq(&db.cycle_a());
}
#[test]
fn cycle_appears() {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::B);
db.set_b_invokes(CycleQuery::None);
assert!(db.cycle_a().is_ok());
db.set_b_invokes(CycleQuery::A);
tracing::debug!("Set Cycle Leaf");
assert!(db.cycle_a().is_err());
}
#[test]
fn cycle_disappears() {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::B);
db.set_b_invokes(CycleQuery::A);
assert!(db.cycle_a().is_err());
db.set_b_invokes(CycleQuery::None);
assert!(db.cycle_a().is_ok());
}
#[test]
fn cycle_disappears_durability() {
let mut db = DatabaseImpl::default();
db.set_a_invokes_with_durability(CycleQuery::B, Durability::LOW);
db.set_b_invokes_with_durability(CycleQuery::A, Durability::HIGH);
let res = db.cycle_a();
assert!(res.is_err());
db.set_a_invokes_with_durability(CycleQuery::None, Durability::LOW);
let res = db.cycle_b();
assert!(res.is_ok());
}
#[test]
fn cycle_mixed_1() {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::B);
db.set_b_invokes(CycleQuery::C);
db.set_c_invokes(CycleQuery::B);
let u = db.cycle_c();
expect![[r#"
Err(
Error {
cycle: [
"cycles::CycleBQuery::cycle_b(())",
"cycles::CycleCQuery::cycle_c(())",
],
},
)
"#]]
.assert_debug_eq(&u);
}
#[test]
fn cycle_mixed_2() {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::B);
db.set_b_invokes(CycleQuery::C);
db.set_c_invokes(CycleQuery::A);
let u = db.cycle_a();
expect![[r#"
Err(
Error {
cycle: [
"cycles::CycleAQuery::cycle_a(())",
"cycles::CycleBQuery::cycle_b(())",
"cycles::CycleCQuery::cycle_c(())",
],
},
)
"#]]
.assert_debug_eq(&u);
}
#[test]
fn cycle_deterministic_order() {
let db = || {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::B);
db.set_b_invokes(CycleQuery::A);
db
};
let a = db().cycle_a();
let b = db().cycle_b();
expect![[r#"
(
Err(
Error {
cycle: [
"cycles::CycleAQuery::cycle_a(())",
"cycles::CycleBQuery::cycle_b(())",
],
},
),
Err(
Error {
cycle: [
"cycles::CycleAQuery::cycle_a(())",
"cycles::CycleBQuery::cycle_b(())",
],
},
),
)
"#]]
.assert_debug_eq(&(a, b));
}
#[test]
fn cycle_multiple() {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::B);
db.set_b_invokes(CycleQuery::AthenC);
db.set_c_invokes(CycleQuery::B);
let c = db.cycle_c();
let b = db.cycle_b();
let a = db.cycle_a();
expect![[r#"
(
Err(
Error {
cycle: [
"cycles::CycleAQuery::cycle_a(())",
"cycles::CycleBQuery::cycle_b(())",
],
},
),
Err(
Error {
cycle: [
"cycles::CycleAQuery::cycle_a(())",
"cycles::CycleBQuery::cycle_b(())",
],
},
),
Err(
Error {
cycle: [
"cycles::CycleAQuery::cycle_a(())",
"cycles::CycleBQuery::cycle_b(())",
],
},
),
)
"#]]
.assert_debug_eq(&(a, b, c));
}
#[test]
fn cycle_recovery_set_but_not_participating() {
let mut db = DatabaseImpl::default();
db.set_a_invokes(CycleQuery::C);
db.set_c_invokes(CycleQuery::C);
let r = extract_cycle(|| drop(db.cycle_a()));
expect![[r#"
[
"cycles::CycleCQuery::cycle_c(())",
]
"#]]
.assert_debug_eq(&r.all_participants(&db));
}