mirror of
https://github.com/salsa-rs/salsa.git
synced 2025-01-23 05:07:27 +00:00
a5395665ce
We used to sort just by the ingredient index, but since those are now added dynamically, that can be fairly unstable in some of the tests. We now sort by the "debug name" of the ingredient first, which is more reliably stable.
450 lines
12 KiB
Rust
450 lines
12 KiB
Rust
#![allow(warnings)]
|
|
|
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
|
|
|
use expect_test::expect;
|
|
use salsa::Durability;
|
|
|
|
// Axes:
|
|
//
|
|
// Threading
|
|
// * Intra-thread
|
|
// * Cross-thread -- part of cycle is on one thread, part on another
|
|
//
|
|
// Recovery strategies:
|
|
// * Panic
|
|
// * Fallback
|
|
// * Mixed -- multiple strategies within cycle participants
|
|
//
|
|
// Across revisions:
|
|
// * N/A -- only one revision
|
|
// * Present in new revision, not old
|
|
// * Present in old revision, not new
|
|
// * Present in both revisions
|
|
//
|
|
// Dependencies
|
|
// * Tracked
|
|
// * Untracked -- cycle participant(s) contain untracked reads
|
|
//
|
|
// Layers
|
|
// * Direct -- cycle participant is directly invoked from test
|
|
// * Indirect -- invoked a query that invokes the cycle
|
|
//
|
|
//
|
|
// | Thread | Recovery | Old, New | Dep style | Layers | Test Name |
|
|
// | ------ | -------- | -------- | --------- | ------ | --------- |
|
|
// | Intra | Panic | N/A | Tracked | direct | cycle_memoized |
|
|
// | Intra | Panic | N/A | Untracked | direct | cycle_volatile |
|
|
// | Intra | Fallback | N/A | Tracked | direct | cycle_cycle |
|
|
// | Intra | Fallback | N/A | Tracked | indirect | inner_cycle |
|
|
// | Intra | Fallback | Both | Tracked | direct | cycle_revalidate |
|
|
// | Intra | Fallback | New | Tracked | direct | cycle_appears |
|
|
// | Intra | Fallback | Old | Tracked | direct | cycle_disappears |
|
|
// | Intra | Fallback | Old | Tracked | direct | cycle_disappears_durability |
|
|
// | Intra | Mixed | N/A | Tracked | direct | cycle_mixed_1 |
|
|
// | Intra | Mixed | N/A | Tracked | direct | cycle_mixed_2 |
|
|
// | Cross | Panic | N/A | Tracked | both | parallel/parallel_cycle_none_recover.rs |
|
|
// | Cross | Fallback | N/A | Tracked | both | parallel/parallel_cycle_one_recover.rs |
|
|
// | Cross | Fallback | N/A | Tracked | both | parallel/parallel_cycle_mid_recover.rs |
|
|
// | Cross | Fallback | N/A | Tracked | both | parallel/parallel_cycle_all_recover.rs |
|
|
|
|
// TODO: The following test is not yet ported.
|
|
// | Intra | Fallback | Old | Tracked | direct | cycle_disappears_durability |
|
|
|
|
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
|
struct Error {
|
|
cycle: Vec<String>,
|
|
}
|
|
|
|
use salsa::Database as Db;
|
|
use salsa::Setter;
|
|
|
|
#[salsa::db]
|
|
#[derive(Default)]
|
|
struct Database {
|
|
storage: salsa::Storage<Self>,
|
|
}
|
|
|
|
#[salsa::db]
|
|
impl salsa::Database for Database {}
|
|
|
|
impl RefUnwindSafe for Database {}
|
|
|
|
#[salsa::input]
|
|
struct MyInput {}
|
|
|
|
#[salsa::tracked]
|
|
fn memoized_a(db: &dyn Db, input: MyInput) {
|
|
memoized_b(db, input)
|
|
}
|
|
|
|
#[salsa::tracked]
|
|
fn memoized_b(db: &dyn Db, input: MyInput) {
|
|
memoized_a(db, input)
|
|
}
|
|
|
|
#[salsa::tracked]
|
|
fn volatile_a(db: &dyn Db, input: MyInput) {
|
|
db.report_untracked_read();
|
|
volatile_b(db, input)
|
|
}
|
|
|
|
#[salsa::tracked]
|
|
fn volatile_b(db: &dyn Db, input: MyInput) {
|
|
db.report_untracked_read();
|
|
volatile_a(db, input)
|
|
}
|
|
|
|
/// The queries A, B, and C in `Database` can be configured
|
|
/// to invoke one another in arbitrary ways using this
|
|
/// enum.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
enum CycleQuery {
|
|
None,
|
|
A,
|
|
B,
|
|
C,
|
|
AthenC,
|
|
}
|
|
|
|
#[salsa::input]
|
|
struct ABC {
|
|
a: CycleQuery,
|
|
b: CycleQuery,
|
|
c: CycleQuery,
|
|
}
|
|
|
|
impl CycleQuery {
|
|
fn invoke(self, db: &dyn Db, abc: ABC) -> Result<(), Error> {
|
|
match self {
|
|
CycleQuery::A => cycle_a(db, abc),
|
|
CycleQuery::B => cycle_b(db, abc),
|
|
CycleQuery::C => cycle_c(db, abc),
|
|
CycleQuery::AthenC => {
|
|
let _ = cycle_a(db, abc);
|
|
cycle_c(db, abc)
|
|
}
|
|
CycleQuery::None => Ok(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[salsa::tracked(recovery_fn=recover_a)]
|
|
fn cycle_a(db: &dyn Db, abc: ABC) -> Result<(), Error> {
|
|
abc.a(db).invoke(db, abc)
|
|
}
|
|
|
|
fn recover_a(db: &dyn Db, cycle: &salsa::Cycle, abc: ABC) -> Result<(), Error> {
|
|
Err(Error {
|
|
cycle: cycle.participant_keys().map(|k| format!("{k:?}")).collect(),
|
|
})
|
|
}
|
|
|
|
#[salsa::tracked(recovery_fn=recover_b)]
|
|
fn cycle_b(db: &dyn Db, abc: ABC) -> Result<(), Error> {
|
|
abc.b(db).invoke(db, abc)
|
|
}
|
|
|
|
fn recover_b(db: &dyn Db, cycle: &salsa::Cycle, abc: ABC) -> Result<(), Error> {
|
|
Err(Error {
|
|
cycle: cycle.participant_keys().map(|k| format!("{k:?}")).collect(),
|
|
})
|
|
}
|
|
|
|
#[salsa::tracked]
|
|
fn cycle_c(db: &dyn Db, abc: ABC) -> Result<(), Error> {
|
|
abc.c(db).invoke(db, abc)
|
|
}
|
|
|
|
#[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() {
|
|
Database::default().attach(|db| {
|
|
let input = MyInput::new(db);
|
|
let cycle = extract_cycle(|| memoized_a(db, input));
|
|
let expected = expect![[r#"
|
|
[
|
|
memoized_a(0),
|
|
memoized_b(0),
|
|
]
|
|
"#]];
|
|
expected.assert_debug_eq(&cycle.all_participants(db));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_volatile() {
|
|
Database::default().attach(|db| {
|
|
let input = MyInput::new(db);
|
|
let cycle = extract_cycle(|| volatile_a(db, input));
|
|
let expected = expect![[r#"
|
|
[
|
|
volatile_a(0),
|
|
volatile_b(0),
|
|
]
|
|
"#]];
|
|
expected.assert_debug_eq(&cycle.all_participants(db));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn expect_cycle() {
|
|
// A --> B
|
|
// ^ |
|
|
// +-----+
|
|
|
|
Database::default().attach(|db| {
|
|
let abc = ABC::new(db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
|
|
assert!(cycle_a(db, abc).is_err());
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn inner_cycle() {
|
|
// A --> B <-- C
|
|
// ^ |
|
|
// +-----+
|
|
Database::default().attach(|db| {
|
|
let abc = ABC::new(db, CycleQuery::B, CycleQuery::A, CycleQuery::B);
|
|
let err = cycle_c(db, abc);
|
|
assert!(err.is_err());
|
|
let expected = expect![[r#"
|
|
[
|
|
"cycle_a(0)",
|
|
"cycle_b(0)",
|
|
]
|
|
"#]];
|
|
expected.assert_debug_eq(&err.unwrap_err().cycle);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_revalidate() {
|
|
// A --> B
|
|
// ^ |
|
|
// +-----+
|
|
let mut db = Database::default();
|
|
let abc = ABC::new(&db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
|
|
assert!(cycle_a(&db, abc).is_err());
|
|
abc.set_b(&mut db).to(CycleQuery::A); // same value as default
|
|
assert!(cycle_a(&db, abc).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_recovery_unchanged_twice() {
|
|
// A --> B
|
|
// ^ |
|
|
// +-----+
|
|
let mut db = Database::default();
|
|
let abc = ABC::new(&db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
|
|
assert!(cycle_a(&db, abc).is_err());
|
|
|
|
abc.set_c(&mut db).to(CycleQuery::A); // force new revision
|
|
assert!(cycle_a(&db, abc).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_appears() {
|
|
let mut db = Database::default();
|
|
|
|
// A --> B
|
|
let abc = ABC::new(&db, CycleQuery::B, CycleQuery::None, CycleQuery::None);
|
|
assert!(cycle_a(&db, abc).is_ok());
|
|
|
|
// A --> B
|
|
// ^ |
|
|
// +-----+
|
|
abc.set_b(&mut db).to(CycleQuery::A);
|
|
assert!(cycle_a(&db, abc).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_disappears() {
|
|
let mut db = Database::default();
|
|
|
|
// A --> B
|
|
// ^ |
|
|
// +-----+
|
|
let abc = ABC::new(&db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
|
|
assert!(cycle_a(&db, abc).is_err());
|
|
|
|
// A --> B
|
|
abc.set_b(&mut db).to(CycleQuery::None);
|
|
assert!(cycle_a(&db, abc).is_ok());
|
|
}
|
|
|
|
/// A variant on `cycle_disappears` in which the values of
|
|
/// `a` and `b` are set with durability values.
|
|
/// If we are not careful, this could cause us to overlook
|
|
/// the fact that the cycle will no longer occur.
|
|
#[test]
|
|
fn cycle_disappears_durability() {
|
|
let mut db = Database::default();
|
|
let abc = ABC::new(
|
|
&mut db,
|
|
CycleQuery::None,
|
|
CycleQuery::None,
|
|
CycleQuery::None,
|
|
);
|
|
abc.set_a(&mut db)
|
|
.with_durability(Durability::LOW)
|
|
.to(CycleQuery::B);
|
|
abc.set_b(&mut db)
|
|
.with_durability(Durability::HIGH)
|
|
.to(CycleQuery::A);
|
|
|
|
assert!(cycle_a(&db, abc).is_err());
|
|
|
|
// At this point, `a` read `LOW` input, and `b` read `HIGH` input. However,
|
|
// because `b` participates in the same cycle as `a`, its final durability
|
|
// should be `LOW`.
|
|
//
|
|
// Check that setting a `LOW` input causes us to re-execute `b` query, and
|
|
// observe that the cycle goes away.
|
|
abc.set_a(&mut db)
|
|
.with_durability(Durability::LOW)
|
|
.to(CycleQuery::None);
|
|
|
|
assert!(cycle_b(&mut db, abc).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_mixed_1() {
|
|
Database::default().attach(|db| {
|
|
// A --> B <-- C
|
|
// | ^
|
|
// +-----+
|
|
let abc = ABC::new(db, CycleQuery::B, CycleQuery::C, CycleQuery::B);
|
|
|
|
let expected = expect![[r#"
|
|
[
|
|
"cycle_b(0)",
|
|
"cycle_c(0)",
|
|
]
|
|
"#]];
|
|
expected.assert_debug_eq(&cycle_c(db, abc).unwrap_err().cycle);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_mixed_2() {
|
|
Database::default().attach(|db| {
|
|
// Configuration:
|
|
//
|
|
// A --> B --> C
|
|
// ^ |
|
|
// +-----------+
|
|
let abc = ABC::new(db, CycleQuery::B, CycleQuery::C, CycleQuery::A);
|
|
let expected = expect![[r#"
|
|
[
|
|
"cycle_a(0)",
|
|
"cycle_b(0)",
|
|
"cycle_c(0)",
|
|
]
|
|
"#]];
|
|
expected.assert_debug_eq(&cycle_a(db, abc).unwrap_err().cycle);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_deterministic_order() {
|
|
// No matter whether we start from A or B, we get the same set of participants:
|
|
let f = || {
|
|
let mut db = Database::default();
|
|
|
|
// A --> B
|
|
// ^ |
|
|
// +-----+
|
|
let abc = ABC::new(&db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
|
|
(db, abc)
|
|
};
|
|
let (db, abc) = f();
|
|
let a = cycle_a(&db, abc);
|
|
let (db, abc) = f();
|
|
let b = cycle_b(&db, abc);
|
|
let expected = expect![[r#"
|
|
(
|
|
[
|
|
"cycle_a(0)",
|
|
"cycle_b(0)",
|
|
],
|
|
[
|
|
"cycle_a(0)",
|
|
"cycle_b(0)",
|
|
],
|
|
)
|
|
"#]];
|
|
expected.assert_debug_eq(&(a.unwrap_err().cycle, b.unwrap_err().cycle));
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_multiple() {
|
|
// No matter whether we start from A or B, we get the same set of participants:
|
|
let mut db = Database::default();
|
|
|
|
// Configuration:
|
|
//
|
|
// A --> B <-- C
|
|
// ^ | ^
|
|
// +-----+ |
|
|
// | |
|
|
// +-----+
|
|
//
|
|
// Here, conceptually, B encounters a cycle with A and then
|
|
// recovers.
|
|
let abc = ABC::new(&db, CycleQuery::B, CycleQuery::AthenC, CycleQuery::A);
|
|
|
|
let c = cycle_c(&db, abc);
|
|
let b = cycle_b(&db, abc);
|
|
let a = cycle_a(&db, abc);
|
|
let expected = expect![[r#"
|
|
(
|
|
[
|
|
"cycle_a(0)",
|
|
"cycle_b(0)",
|
|
],
|
|
[
|
|
"cycle_a(0)",
|
|
"cycle_b(0)",
|
|
],
|
|
[
|
|
"cycle_a(0)",
|
|
"cycle_b(0)",
|
|
],
|
|
)
|
|
"#]];
|
|
expected.assert_debug_eq(&(
|
|
c.unwrap_err().cycle,
|
|
b.unwrap_err().cycle,
|
|
a.unwrap_err().cycle,
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_recovery_set_but_not_participating() {
|
|
Database::default().attach(|db| {
|
|
// A --> C -+
|
|
// ^ |
|
|
// +--+
|
|
let abc = ABC::new(db, CycleQuery::C, CycleQuery::None, CycleQuery::C);
|
|
|
|
// Here we expect C to panic and A not to recover:
|
|
let r = extract_cycle(|| drop(cycle_a(db, abc)));
|
|
let expected = expect![[r#"
|
|
[
|
|
cycle_c(0),
|
|
]
|
|
"#]];
|
|
expected.assert_debug_eq(&r.all_participants(db));
|
|
})
|
|
}
|