Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ra-salsa/tests/cycles.rs')
-rw-r--r--crates/ra-salsa/tests/cycles.rs492
1 files changed, 492 insertions, 0 deletions
diff --git a/crates/ra-salsa/tests/cycles.rs b/crates/ra-salsa/tests/cycles.rs
new file mode 100644
index 0000000000..8113662655
--- /dev/null
+++ b/crates/ra-salsa/tests/cycles.rs
@@ -0,0 +1,492 @@
+use std::panic::UnwindSafe;
+
+use expect_test::expect;
+use ra_salsa::{Durability, ParallelDatabase, Snapshot};
+
+// 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 | Fallback | N/A | Tracked | both | parallel/cycles.rs: recover_parallel_cycle |
+// | Cross | Panic | N/A | Tracked | both | parallel/cycles.rs: panic_parallel_cycle |
+
+#[derive(PartialEq, Eq, Hash, Clone, Debug)]
+struct Error {
+ cycle: Vec<String>,
+}
+
+#[ra_salsa::database(GroupStruct)]
+#[derive(Default)]
+struct DatabaseImpl {
+ storage: ra_salsa::Storage<Self>,
+}
+
+impl ra_salsa::Database for DatabaseImpl {}
+
+impl ParallelDatabase for DatabaseImpl {
+ fn snapshot(&self) -> Snapshot<Self> {
+ Snapshot::new(DatabaseImpl { storage: self.storage.snapshot() })
+ }
+}
+
+/// 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,
+}
+
+#[ra_salsa::query_group(GroupStruct)]
+trait Database: ra_salsa::Database {
+ // `a` and `b` depend on each other and form a cycle
+ fn memoized_a(&self) -> ();
+ fn memoized_b(&self) -> ();
+ fn volatile_a(&self) -> ();
+ fn volatile_b(&self) -> ();
+
+ #[ra_salsa::input]
+ fn a_invokes(&self) -> CycleQuery;
+
+ #[ra_salsa::input]
+ fn b_invokes(&self) -> CycleQuery;
+
+ #[ra_salsa::input]
+ fn c_invokes(&self) -> CycleQuery;
+
+ #[ra_salsa::cycle(recover_a)]
+ fn cycle_a(&self) -> Result<(), Error>;
+
+ #[ra_salsa::cycle(recover_b)]
+ fn cycle_b(&self) -> Result<(), Error>;
+
+ fn cycle_c(&self) -> Result<(), Error>;
+}
+
+fn recover_a(db: &dyn Database, cycle: &ra_salsa::Cycle) -> Result<(), Error> {
+ Err(Error { cycle: cycle.all_participants(db) })
+}
+
+fn recover_b(db: &dyn Database, cycle: &ra_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) -> ra_salsa::Cycle {
+ let v = std::panic::catch_unwind(f);
+ if let Err(d) = &v {
+ if let Some(cycle) = d.downcast_ref::<ra_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();
+
+ // A --> B
+ // ^ |
+ // +-----+
+
+ 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();
+
+ // A --> B <-- C
+ // ^ |
+ // +-----+
+
+ 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();
+
+ // A --> B
+ // ^ |
+ // +-----+
+ db.set_a_invokes(CycleQuery::B);
+ db.set_b_invokes(CycleQuery::A);
+
+ assert!(db.cycle_a().is_err());
+ db.set_b_invokes(CycleQuery::A); // same value as default
+ assert!(db.cycle_a().is_err());
+}
+
+#[test]
+fn cycle_revalidate_unchanged_twice() {
+ let mut db = DatabaseImpl::default();
+
+ // A --> B
+ // ^ |
+ // +-----+
+ db.set_a_invokes(CycleQuery::B);
+ db.set_b_invokes(CycleQuery::A);
+
+ assert!(db.cycle_a().is_err());
+ db.set_c_invokes(CycleQuery::A); // force new revisi5on
+
+ // on this run
+ 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();
+
+ // A --> B
+ db.set_a_invokes(CycleQuery::B);
+ db.set_b_invokes(CycleQuery::None);
+ assert!(db.cycle_a().is_ok());
+
+ // A --> B
+ // ^ |
+ // +-----+
+ 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();
+
+ // A --> B
+ // ^ |
+ // +-----+
+ db.set_a_invokes(CycleQuery::B);
+ db.set_b_invokes(CycleQuery::A);
+ assert!(db.cycle_a().is_err());
+
+ // A --> B
+ db.set_b_invokes(CycleQuery::None);
+ assert!(db.cycle_a().is_ok());
+}
+
+/// A variant on `cycle_disappears` in which the values of
+/// `a_invokes` and `b_invokes` 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 = 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());
+
+ // 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.
+ 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();
+ // A --> B <-- C
+ // | ^
+ // +-----+
+ 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();
+
+ // Configuration:
+ //
+ // A --> B --> C
+ // ^ |
+ // +-----------+
+ 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() {
+ // No matter whether we start from A or B, we get the same set of participants:
+ let db = || {
+ let mut db = DatabaseImpl::default();
+ // A --> B
+ // ^ |
+ // +-----+
+ 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() {
+ // No matter whether we start from A or B, we get the same set of participants:
+ let mut db = DatabaseImpl::default();
+
+ // Configuration:
+ //
+ // A --> B <-- C
+ // ^ | ^
+ // +-----+ |
+ // | |
+ // +-----+
+ //
+ // Here, conceptually, B encounters a cycle with A and then
+ // recovers.
+ 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();
+
+ // A --> C -+
+ // ^ |
+ // +--+
+ db.set_a_invokes(CycleQuery::C);
+ db.set_c_invokes(CycleQuery::C);
+
+ // Here we expect C to panic and A not to recover:
+ let r = extract_cycle(|| drop(db.cycle_a()));
+ expect![[r#"
+ [
+ "cycles::CycleCQuery::cycle_c(())",
+ ]
+ "#]]
+ .assert_debug_eq(&r.all_participants(&db));
+}