diff --git a/cpp/include/loot/database_interface.h b/cpp/include/loot/database_interface.h index 69242714..75d85759 100644 --- a/cpp/include/loot/database_interface.h +++ b/cpp/include/loot/database_interface.h @@ -120,6 +120,18 @@ public: */ virtual bool Evaluate(const std::string& condition) const = 0; + /** + * @brief Clears the cache of metadata condition evaluation results. + * @details As many conditions involve reading files and/or directories, + * libloot caches the results of condition evaluation and reuses + * those cached results in subsequent evaluations. + * + * Clearing the condition cache means that the next time a condition + * is evaluated, it will be evaluated from scratch instead of using a + * cached result. + */ + virtual void ClearConditionCache() = 0; + /** * @} * @name Non-plugin Data Access diff --git a/cpp/src/api/database.cpp b/cpp/src/api/database.cpp index 8a2003e6..239ab176 100644 --- a/cpp/src/api/database.cpp +++ b/cpp/src/api/database.cpp @@ -52,6 +52,14 @@ bool Database::Evaluate(const std::string& condition) const { } } +void Database::ClearConditionCache() { + try { + return database_->clear_condition_cache(); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } +} + std::vector Database::GetKnownBashTags() const { try { return convert(database_->known_bash_tags()); diff --git a/cpp/src/api/database.h b/cpp/src/api/database.h index 34a370b0..b05899d5 100644 --- a/cpp/src/api/database.h +++ b/cpp/src/api/database.h @@ -26,6 +26,8 @@ public: bool Evaluate(const std::string& condition) const override; + void ClearConditionCache() override; + std::vector GetKnownBashTags() const override; std::vector GetGeneralMessages( diff --git a/cpp/src/database.rs b/cpp/src/database.rs index e5c887be..067e6b3d 100644 --- a/cpp/src/database.rs +++ b/cpp/src/database.rs @@ -94,6 +94,15 @@ impl Database { .map_err(Into::into) } + pub fn clear_condition_cache(&self) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(DatabaseLockPoisonError::from)? + .clear_condition_cache(); + + Ok(()) + } + pub fn known_bash_tags(&self) -> Result, VerboseError> { Ok(self .0 diff --git a/cpp/src/lib.rs b/cpp/src/lib.rs index c83adc41..1e0c128b 100644 --- a/cpp/src/lib.rs +++ b/cpp/src/lib.rs @@ -266,6 +266,8 @@ mod ffi { pub fn evaluate(&self, condition: &str) -> Result; + pub fn clear_condition_cache(&self) -> Result<()>; + pub fn known_bash_tags(&self) -> Result>; pub fn general_messages(&self, evaluate_conditions: bool) -> Result>; diff --git a/cpp/src/tests/api/interface/database_interface_test.h b/cpp/src/tests/api/interface/database_interface_test.h index c1bda817..4a9fe169 100644 --- a/cpp/src/tests/api/interface/database_interface_test.h +++ b/cpp/src/tests/api/interface/database_interface_test.h @@ -277,6 +277,21 @@ TEST_P(DatabaseInterfaceTest, evaluateShouldReturnFalseIfTheConditionIsFalse) { EXPECT_FALSE(handle_->GetDatabase().Evaluate("file(\"missing.esp\")")); } +TEST_P(DatabaseInterfaceTest, + clearConditionCacheShouldCauseConditionsToBeEvaluatedFromScratch) { + const auto condition = "file(\"Blank.esp\")"; + + EXPECT_TRUE(handle_->GetDatabase().Evaluate(condition)); + + std::filesystem::remove(dataPath / "Blank.esp"); + + EXPECT_TRUE(handle_->GetDatabase().Evaluate(condition)); + + handle_->GetDatabase().ClearConditionCache(); + + EXPECT_FALSE(handle_->GetDatabase().Evaluate(condition)); +} + TEST_P(DatabaseInterfaceTest, getGroupsShouldReturnAllGroupsListedInTheLoadedMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); diff --git a/nodejs/src/database.rs b/nodejs/src/database.rs index e5567e01..2815fd92 100644 --- a/nodejs/src/database.rs +++ b/nodejs/src/database.rs @@ -128,6 +128,16 @@ impl Database { .map_err(Into::into) } + #[napi] + pub fn clear_condition_cache(&self) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(DatabaseLockPoisonError::from)? + .clear_condition_cache(); + + Ok(()) + } + #[napi] pub fn known_bash_tags(&self) -> Result, VerboseError> { Ok(self diff --git a/python/src/database.rs b/python/src/database.rs index 75408ad4..ec1a8644 100644 --- a/python/src/database.rs +++ b/python/src/database.rs @@ -98,6 +98,15 @@ impl Database { .map_err(Into::into) } + pub fn clear_condition_cache(&self) -> Result<(), VerboseError> { + self.0 + .write() + .map_err(DatabaseLockPoisonError::from)? + .clear_condition_cache(); + + Ok(()) + } + pub fn known_bash_tags(&self) -> Result, VerboseError> { Ok(self .0 diff --git a/src/database/mod.rs b/src/database/mod.rs index 1182f497..f3f1b840 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -75,13 +75,6 @@ impl Database { &mut self.condition_evaluator_state } - pub(crate) fn clear_condition_cache(&mut self) { - if let Err(e) = self.condition_evaluator_state.clear_condition_cache() { - logging::error!("The condition cache's lock is poisoned, assigning a new cache"); - *e.into_inner() = HashMap::new(); - } - } - /// Loads the masterlist from the given path. /// /// Replaces any existing data that was previously loaded from a masterlist. @@ -154,6 +147,22 @@ impl Database { evaluate_condition(condition, &self.condition_evaluator_state).map_err(Into::into) } + /// Clears the cache of metadata condition evaluation results. + /// + /// As many conditions involve reading files and/or directories, libloot + /// caches the results of condition evaluation and reuses those cached + /// results in subsequent evaluations. + /// + /// Clearing the condition cache means that the next time a condition is + /// evaluated, it will be evaluated from scratch instead of using a cached + /// result. + pub fn clear_condition_cache(&mut self) { + if let Err(e) = self.condition_evaluator_state.clear_condition_cache() { + logging::error!("The condition cache's lock is poisoned, assigning a new cache"); + *e.into_inner() = HashMap::new(); + } + } + /// Gets the Bash Tags that are listed in the loaded metadata lists. /// /// Bash Tag suggestions can include Bash Tags not in this list. @@ -165,17 +174,10 @@ impl Database { } /// Get all general messages listed in the loaded metadata lists. - /// - /// Evaluating general message conditions also clears the condition cache - /// before evaluating conditions. pub fn general_messages( - &mut self, + &self, evaluate_conditions: EvalMode, ) -> Result, ConditionEvaluationError> { - if evaluate_conditions == EvalMode::Evaluate { - self.clear_condition_cache(); - } - let messages_iter = self .masterlist .messages() @@ -237,9 +239,6 @@ impl Database { } /// Get all of a plugin's loaded metadata. - /// - /// Evaluating plugin metadata conditions does **not** clear the condition - /// cache. pub fn plugin_metadata( &self, plugin_name: &str, @@ -267,9 +266,6 @@ impl Database { } /// Get a plugin's metadata loaded from the given userlist. - /// - /// Evaluating plugin metadata conditions does **not** clear the condition - /// cache. pub fn plugin_user_metadata( &self, plugin_name: &str, @@ -838,6 +834,43 @@ plugins: } } + mod evaluate { + use super::*; + + #[test] + fn should_return_true_if_the_condition_is_true() { + let fixture = Fixture::new(GameType::Oblivion); + let database = fixture.database(); + + assert!(database.evaluate("file(\"Blank.esp\")").unwrap()); + } + + #[test] + fn should_return_false_if_the_condition_is_false() { + let fixture = Fixture::new(GameType::Oblivion); + let database = fixture.database(); + + assert!(!database.evaluate("file(\"missing.esp\")").unwrap()); + } + } + + #[test] + fn clear_condition_cache_should_cause_conditions_to_be_evaluated_from_scratch() { + let fixture = Fixture::new(GameType::Oblivion); + let condition = "file(\"Blank.esp\")"; + let mut database = fixture.database(); + + assert!(database.evaluate(condition).unwrap()); + + std::fs::remove_file(fixture.inner.data_path().join("Blank.esp")).unwrap(); + + assert!(database.evaluate(condition).unwrap()); + + database.clear_condition_cache(); + + assert!(!database.evaluate(condition).unwrap()); + } + #[test] fn known_bash_tags_should_append_userlist_tags_to_masterlist_tags() { let fixture = Fixture::new(GameType::Oblivion); @@ -914,26 +947,6 @@ plugins: } } - mod evaluate { - use super::*; - - #[test] - fn should_return_true_if_the_condition_is_true() { - let fixture = Fixture::new(GameType::Oblivion); - let database = fixture.database(); - - assert!(database.evaluate("file(\"Blank.esp\")").unwrap()); - } - - #[test] - fn should_return_false_if_the_condition_is_false() { - let fixture = Fixture::new(GameType::Oblivion); - let database = fixture.database(); - - assert!(!database.evaluate("file(\"missing.esp\")").unwrap()); - } - } - mod groups { use super::*;