Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/query-group-macro/tests/cycle.rs')
-rw-r--r--crates/query-group-macro/tests/cycle.rs275
1 files changed, 275 insertions, 0 deletions
diff --git a/crates/query-group-macro/tests/cycle.rs b/crates/query-group-macro/tests/cycle.rs
new file mode 100644
index 0000000000..12df4ae3ef
--- /dev/null
+++ b/crates/query-group-macro/tests/cycle.rs
@@ -0,0 +1,275 @@
+use std::panic::UnwindSafe;
+
+use expect_test::expect;
+use query_group_macro::query_group;
+use salsa::Setter;
+
+/// 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 CycleDatabase, abc: ABC) -> Result<(), Error> {
+ match self {
+ CycleQuery::A => db.cycle_a(abc),
+ CycleQuery::B => db.cycle_b(abc),
+ CycleQuery::C => db.cycle_c(abc),
+ CycleQuery::AthenC => {
+ let _ = db.cycle_a(abc);
+ db.cycle_c(abc)
+ }
+ CycleQuery::None => Ok(()),
+ }
+ }
+}
+
+#[salsa::input]
+struct MyInput {}
+
+#[salsa::tracked]
+fn memoized_a(db: &dyn CycleDatabase, input: MyInput) {
+ memoized_b(db, input)
+}
+
+#[salsa::tracked]
+fn memoized_b(db: &dyn CycleDatabase, input: MyInput) {
+ memoized_a(db, input)
+}
+
+#[salsa::tracked]
+fn volatile_a(db: &dyn CycleDatabase, input: MyInput) {
+ db.report_untracked_read();
+ volatile_b(db, input)
+}
+
+#[salsa::tracked]
+fn volatile_b(db: &dyn CycleDatabase, input: MyInput) {
+ db.report_untracked_read();
+ volatile_a(db, input)
+}
+
+#[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)
+}
+
+#[derive(PartialEq, Eq, Hash, Clone, Debug)]
+struct Error {
+ cycle: Vec<String>,
+}
+
+#[query_group]
+trait CycleDatabase: salsa::Database {
+ #[salsa::cycle(recover_a)]
+ fn cycle_a(&self, abc: ABC) -> Result<(), Error>;
+
+ #[salsa::cycle(recover_b)]
+ fn cycle_b(&self, abc: ABC) -> Result<(), Error>;
+
+ fn cycle_c(&self, abc: ABC) -> Result<(), Error>;
+}
+
+fn cycle_a(db: &dyn CycleDatabase, abc: ABC) -> Result<(), Error> {
+ abc.a(db).invoke(db, abc)
+}
+
+fn recover_a(
+ _db: &dyn CycleDatabase,
+ cycle: &salsa::Cycle,
+ _: CycleDatabaseData,
+ _abc: ABC,
+) -> Result<(), Error> {
+ Err(Error { cycle: cycle.participant_keys().map(|k| format!("{k:?}")).collect() })
+}
+
+fn cycle_b(db: &dyn CycleDatabase, abc: ABC) -> Result<(), Error> {
+ abc.b(db).invoke(db, abc)
+}
+
+fn recover_b(
+ _db: &dyn CycleDatabase,
+ cycle: &salsa::Cycle,
+ _: CycleDatabaseData,
+ _abc: ABC,
+) -> Result<(), Error> {
+ Err(Error { cycle: cycle.participant_keys().map(|k| format!("{k:?}")).collect() })
+}
+
+fn cycle_c(db: &dyn CycleDatabase, abc: ABC) -> Result<(), Error> {
+ abc.c(db).invoke(db, abc)
+}
+
+#[test]
+fn cycle_memoized() {
+ let db = salsa::DatabaseImpl::new();
+
+ let input = MyInput::new(&db);
+ let cycle = extract_cycle(|| memoized_a(&db, input));
+ let expected = expect![[r#"
+ [
+ DatabaseKeyIndex(
+ IngredientIndex(
+ 1,
+ ),
+ Id(0),
+ ),
+ DatabaseKeyIndex(
+ IngredientIndex(
+ 2,
+ ),
+ Id(0),
+ ),
+ ]
+ "#]];
+ expected.assert_debug_eq(&cycle.all_participants(&db));
+}
+
+#[test]
+fn inner_cycle() {
+ // A --> B <-- C
+ // ^ |
+ // +-----+
+ let db = salsa::DatabaseImpl::new();
+
+ let abc = ABC::new(&db, CycleQuery::B, CycleQuery::A, CycleQuery::B);
+ let err = db.cycle_c(abc);
+ assert!(err.is_err());
+ let expected = expect![[r#"
+ [
+ "cycle_a_shim(Id(1400))",
+ "cycle_b_shim(Id(1000))",
+ ]
+ "#]];
+ expected.assert_debug_eq(&err.unwrap_err().cycle);
+}
+
+#[test]
+fn cycle_revalidate() {
+ // A --> B
+ // ^ |
+ // +-----+
+ let mut db = salsa::DatabaseImpl::new();
+ let abc = ABC::new(&db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
+ assert!(db.cycle_a(abc).is_err());
+ abc.set_b(&mut db).to(CycleQuery::A); // same value as default
+ assert!(db.cycle_a(abc).is_err());
+}
+
+#[test]
+fn cycle_recovery_unchanged_twice() {
+ // A --> B
+ // ^ |
+ // +-----+
+ let mut db = salsa::DatabaseImpl::new();
+ let abc = ABC::new(&db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
+ assert!(db.cycle_a(abc).is_err());
+
+ abc.set_c(&mut db).to(CycleQuery::A); // force new revision
+ assert!(db.cycle_a(abc).is_err());
+}
+
+#[test]
+fn cycle_appears() {
+ let mut db = salsa::DatabaseImpl::new();
+ // A --> B
+ let abc = ABC::new(&db, CycleQuery::B, CycleQuery::None, CycleQuery::None);
+ assert!(db.cycle_a(abc).is_ok());
+
+ // A --> B
+ // ^ |
+ // +-----+
+ abc.set_b(&mut db).to(CycleQuery::A);
+ assert!(db.cycle_a(abc).is_err());
+}
+
+#[test]
+fn cycle_disappears() {
+ let mut db = salsa::DatabaseImpl::new();
+
+ // A --> B
+ // ^ |
+ // +-----+
+ let abc = ABC::new(&db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
+ assert!(db.cycle_a(abc).is_err());
+
+ // A --> B
+ abc.set_b(&mut db).to(CycleQuery::None);
+ assert!(db.cycle_a(abc).is_ok());
+}
+
+#[test]
+fn cycle_multiple() {
+ // No matter whether we start from A or B, we get the same set of participants:
+ let db = salsa::DatabaseImpl::new();
+
+ // 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 = db.cycle_c(abc);
+ let b = db.cycle_b(abc);
+ let a = db.cycle_a(abc);
+ let expected = expect![[r#"
+ (
+ [
+ "cycle_a_shim(Id(1000))",
+ "cycle_b_shim(Id(1400))",
+ ],
+ [
+ "cycle_a_shim(Id(1000))",
+ "cycle_b_shim(Id(1400))",
+ ],
+ [
+ "cycle_a_shim(Id(1000))",
+ "cycle_b_shim(Id(1400))",
+ ],
+ )
+ "#]];
+ expected.assert_debug_eq(&(c.unwrap_err().cycle, b.unwrap_err().cycle, a.unwrap_err().cycle));
+}
+
+#[test]
+fn cycle_mixed_1() {
+ let db = salsa::DatabaseImpl::new();
+ // A --> B <-- C
+ // | ^
+ // +-----+
+ let abc = ABC::new(&db, CycleQuery::B, CycleQuery::C, CycleQuery::B);
+
+ let expected = expect![[r#"
+ [
+ "cycle_b_shim(Id(1000))",
+ "cycle_c_shim(Id(c00))",
+ ]
+ "#]];
+ expected.assert_debug_eq(&db.cycle_c(abc).unwrap_err().cycle);
+}