diff --git a/crates/project/src/data.rs b/crates/project/src/data.rs index 0f32055..8b9b99e 100644 --- a/crates/project/src/data.rs +++ b/crates/project/src/data.rs @@ -198,6 +198,31 @@ impl DataView<'_, M> { })); } + /// Plans to move this data section to another planned document. + /// + /// This will not modify the [`crate::Project`], just record this change to the + /// same [`ChangeBuilder`] [`crate::PlannedDocument`] was created with. + /// + /// # Arguments + /// + /// * `new_owner` - The planned document to move the data to. + /// + /// # Panics + /// If a [`crate::PlannedDocument`] for a different [`crate::Project`] was passed. + pub fn move_to_planned_document(&self, new_owner: &mut crate::PlannedDocument) { + assert!( + new_owner.cb.is_same_source_as(self), + "ChangeBuilder must stem from the same project" + ); + new_owner + .cb + .changes + .push(PendingChange::Change(Change::MoveData { + id: self.id, + new_owner: Some(new_owner.id), + })); + } + /// Plans to make this data section an orphan (not owned by any document). /// /// This will not modify the [`crate::Project`], just record this change to `cb`. diff --git a/crates/project/src/document.rs b/crates/project/src/document.rs index 19aef77..c10afea 100644 --- a/crates/project/src/document.rs +++ b/crates/project/src/document.rs @@ -30,7 +30,7 @@ impl fmt::Display for DocumentId { /// Document in a [`crate::Project`] /// /// Defines the metadata and the identifiers of containing data sections. -#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Document { pub data: Vec, } diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index 589356c..74babc5 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -30,6 +30,7 @@ //! via a [`ModuleRegistry`]. //! - **[`ChangeBuilder`]:** A mechanism for recording changes to be applied to a `Project`, ensuring atomicity and enabling //! undo/redo. +//! - **[`TrackedProjectView`]:** A tracked version of a [`ProjectView`] allowing implicit dependency tracking //! //! ## Usage //! @@ -158,6 +159,7 @@ mod data; mod document; mod module_data; mod project; +mod tracked; mod user; use branch::BranchId; @@ -184,6 +186,11 @@ pub use document::Path; pub use document::PlannedDocument; pub use module_data::ModuleRegistry; pub use project::ProjectView; +pub use tracked::AccessRecorder; +pub use tracked::CacheValidator; +pub use tracked::TrackedDataView; +pub use tracked::TrackedDocumentView; +pub use tracked::TrackedProjectView; pub use user::UserId; /// Facilitates the deserialization of a [`Project`] from a serialized format. diff --git a/crates/project/src/module_data.rs b/crates/project/src/module_data.rs index fd1ada1..04ce285 100644 --- a/crates/project/src/module_data.rs +++ b/crates/project/src/module_data.rs @@ -62,10 +62,13 @@ impl fmt::Display for ModuleId { /// - `TDeserializer` - Deserializer for type-erased data macro_rules! define_type_erased { ($d:ty, $reg_entry:ident) => { + define_type_erased!($d, $reg_entry,); + }; + ($d:ty, $reg_entry:ident, $($extra_traits:path),*) => { paste! { #[doc = "A trait shared by all [`" $d "`] types for all [`Module`]"] #[allow(dead_code)] - pub trait [<$d Trait>]: erased_serde::Serialize + Debug + Send + Sync + Any + DynClone { + pub trait [<$d Trait>]: erased_serde::Serialize + Debug + Send + Sync + Any + DynClone $(+ $extra_traits)* { /// Provides read-only access to the underlying data type. fn as_any(&self) -> &dyn Any; /// Provides mutable access to the underlying data type. @@ -301,6 +304,13 @@ macro_rules! define_type_erased { }; } +pub trait DataCompare { + fn persistent_eq(&self, other: &dyn DataTrait) -> bool; + fn persistent_user_eq(&self, other: &dyn DataTrait) -> bool; + fn session_eq(&self, other: &dyn DataTrait) -> bool; + fn shared_eq(&self, other: &dyn DataTrait) -> bool; +} + /// Complete state of the data of a module, publicly accessible through a [`crate::DataView`]. #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct Data { @@ -309,7 +319,34 @@ pub struct Data { pub session: M::SessionData, pub shared: M::SharedData, } -define_type_erased!(Data, deserialize_data); +define_type_erased!(Data, deserialize_data, DataCompare); + +impl DataCompare for Data { + fn persistent_eq(&self, other: &dyn DataTrait) -> bool { + other + .as_any() + .downcast_ref::() + .is_some_and(|other| self.persistent == other.persistent) + } + fn persistent_user_eq(&self, other: &dyn DataTrait) -> bool { + other + .as_any() + .downcast_ref::() + .is_some_and(|other| self.persistent_user == other.persistent_user) + } + fn session_eq(&self, other: &dyn DataTrait) -> bool { + other + .as_any() + .downcast_ref::() + .is_some_and(|other| self.session == other.session) + } + fn shared_eq(&self, other: &dyn DataTrait) -> bool { + other + .as_any() + .downcast_ref::() + .is_some_and(|other| self.shared == other.shared) + } +} /// Wrapper type around [`Module::SharedData`] #[derive(Clone, Debug, Serialize, Deserialize, Default)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8873993..9a43715 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -34,6 +34,12 @@ impl ProjectSource for ProjectView { } } +impl ProjectSource for std::sync::Arc { + fn uuid(&self) -> uuid::Uuid { + self.uuid + } +} + impl ProjectView { /// Opens a read only [`DocumentView`]. /// diff --git a/crates/project/src/tracked.rs b/crates/project/src/tracked.rs new file mode 100644 index 0000000..800f0a0 --- /dev/null +++ b/crates/project/src/tracked.rs @@ -0,0 +1,588 @@ +use crate::{ + module_data::ModuleId, ChangeBuilder, DataId, DataView, DocumentId, DocumentView, Path, + PlannedData, PlannedDocument, ProjectSource, ProjectView, +}; +use module::{DataSection, Module}; +use std::sync::{Arc, Mutex}; +use uuid::Uuid; + +/// Represents specific events related to accessing project data. +#[derive(Debug, Clone)] +enum AccessEvent { + OpenDocument(DocumentId), + OpenDataById(DataId), + OpenDataByType(ModuleId), + OpenDocumentDataById(DocumentId, DataId), + OpenDocumentDataByType(DocumentId, ModuleId), + AccessPesistent(DataId), + AccessPesistentUser(DataId), + AccessShared(DataId), + AccessSession(DataId), +} + +/// A log of read accesses on a [`TrackedProjectView`] for caching purposes. +#[derive(Clone, Debug)] +pub struct AccessRecorder(Arc>>); + +impl AccessRecorder { + /// Tracks an [`AccessEvent`] by appending it to the log. + /// + /// # Arguments + /// * `access` - The [`AccessEvent`] to track. + fn track(&self, access: AccessEvent) { + self.0.lock().unwrap().push(access); + } + + /// Freezes the current state of tracked read accesses, producing an immutable + /// [`CacheValidator`] that can be used to later check if the same accesses + /// on a different [`ProjectView`] would yield the same results. + /// + /// # Panics + /// + /// This function will panic if: + /// - The internal mutex is poisoned (indicates another thread panicked while holding the lock) + #[must_use] + pub fn freeze(&self) -> CacheValidator { + CacheValidator(std::mem::take( + &mut *self.0.lock().expect("failed to acquire lock"), + )) + } +} + +/// An immutable record of read accesses performed on a [`TrackedProjectView`]. +/// +/// This is used to determine if a cached result derived from that view is still valid. +#[derive(Debug, Clone)] +pub struct CacheValidator(Vec); + +impl CacheValidator { + /// Checks if the accesses in this [`CacheValidator`] yield the same data on two [`ProjectView`]s. + /// + /// This effectively determines if a cache based on these accesses is still valid when comparing + /// a previous project state (`old_view`) to a potentially updated one (`new_view`). + /// + /// # Returns + /// `true` if the cache is still valid, and `false` otherwise. + #[must_use] + #[expect(clippy::too_many_lines)] + pub fn is_cache_valid(&self, old_view: &ProjectView, new_view: &ProjectView) -> bool { + if old_view.uuid != new_view.uuid { + return false; + } + for e in &self.0 { + match e { + AccessEvent::OpenDocument(document_id) => { + if old_view.documents.get(document_id) != new_view.documents.get(document_id) { + return false; + } + } + AccessEvent::OpenDataById(data_id) => { + if old_view.data.get(data_id).map(|d| d.module) + != new_view.data.get(data_id).map(|d| d.module) + { + return false; + } + } + AccessEvent::OpenDataByType(module_id) => { + let old_data_ids = old_view + .data + .iter() + .filter(|d| d.1.module == *module_id) + .map(|d| d.0); + let new_data_ids = new_view + .data + .iter() + .filter(|d| d.1.module == *module_id) + .map(|d| d.0); + if !old_data_ids.eq(new_data_ids) { + return false; + } + } + AccessEvent::OpenDocumentDataById(document_id, data_id) => { + match ( + old_view.documents.get(document_id), + new_view.documents.get(document_id), + ) { + (None, None) => {} + (None, Some(_)) | (Some(_), None) => return false, + (Some(old_doc), Some(new_doc)) => { + if old_doc.data.iter().any(|d| d == data_id) + != new_doc.data.iter().any(|d| d == data_id) + { + return false; + } + } + } + } + AccessEvent::OpenDocumentDataByType(document_id, module_id) => { + match ( + old_view.documents.get(document_id), + new_view.documents.get(document_id), + ) { + (None, None) => {} + (None, Some(_)) | (Some(_), None) => return false, + (Some(old_doc), Some(new_doc)) => { + let old_data_ids = old_doc + .data + .iter() + .map(|d| (d, old_view.data.get(d))) + .filter_map(|d| d.1.map(|ed| (d.0, ed))) + .filter(|d| d.1.module == *module_id) + .map(|d| d.0); + let new_data_ids = new_doc + .data + .iter() + .map(|d| (d, new_view.data.get(d))) + .filter_map(|d| d.1.map(|ed| (d.0, ed))) + .filter(|d| d.1.module == *module_id) + .map(|d| d.0); + if !old_data_ids.eq(new_data_ids) { + return false; + } + } + } + } + AccessEvent::AccessPesistent(data_id) => { + match (old_view.data.get(data_id), new_view.data.get(data_id)) { + (None, None) => {} + (None, Some(_)) | (Some(_), None) => return false, + (Some(old), Some(new)) => { + if old.module != new.module { + return false; + } + if !old.data.persistent_eq(new.data.as_ref()) { + return false; + } + } + } + } + AccessEvent::AccessPesistentUser(data_id) => { + match (old_view.data.get(data_id), new_view.data.get(data_id)) { + (None, None) => {} + (None, Some(_)) | (Some(_), None) => return false, + (Some(old), Some(new)) => { + if old.module != new.module { + return false; + } + if !old.data.persistent_user_eq(new.data.as_ref()) { + return false; + } + } + } + } + AccessEvent::AccessShared(data_id) => { + match (old_view.data.get(data_id), new_view.data.get(data_id)) { + (None, None) => {} + (None, Some(_)) | (Some(_), None) => return false, + (Some(old), Some(new)) => { + if old.module != new.module { + return false; + } + if !old.data.shared_eq(new.data.as_ref()) { + return false; + } + } + } + } + AccessEvent::AccessSession(data_id) => { + match (old_view.data.get(data_id), new_view.data.get(data_id)) { + (None, None) => {} + (None, Some(_)) | (Some(_), None) => return false, + (Some(old), Some(new)) => { + if old.module != new.module { + return false; + } + if !old.data.session_eq(new.data.as_ref()) { + return false; + } + } + } + } + } + } + + true + } + + /// Checks if the associated [`TrackedProjectView`] had any read accesses. + /// + /// # Returns + /// `true` if at least one property about the `project` was queried, and `false` if not. + #[must_use] + pub fn was_accessed(&self) -> bool { + !self.0.is_empty() + } +} + +/// A wrapper around [`ProjectView`] that provides access tracking functionality. +/// +/// [`TrackedProjectView`] is a wrapper around [`ProjectView`] that tracks all read +/// actions on that [`TrackedProjectView`] related [`TrackedDocumentView`] and +/// [`TrackedDataView`]s. +/// +/// This allows implicit dependency tracking of [`ProjectView`]s for caching purposes. +/// +/// Create a new [`TrackedProjectView`] and a [`AccessRecorder`] with [`TrackedProjectView::new`]. +#[derive(Clone, Debug)] +pub struct TrackedProjectView(Arc, AccessRecorder); + +impl ProjectSource for TrackedProjectView { + fn uuid(&self) -> Uuid { + self.0.uuid + } +} + +impl TrackedProjectView { + /// Creates a new [`TrackedProjectView`] from a [`ProjectView`]. + /// + /// # Arguments + /// * `pv` - The [`ProjectView`] to wrap. + /// + /// # Returns + /// A tuple containing the new [`TrackedProjectView`] and its associated [`AccessRecorder`]. + #[must_use] + pub fn new(pv: Arc) -> (Self, AccessRecorder) { + let accesses = AccessRecorder(Arc::new(Mutex::new(Vec::new()))); + (Self(pv, accesses.clone()), accesses) + } + + /// Opens a read only [`TrackedDocumentView`]. + /// + /// # Arguments + /// * `document_id` - The unique identifier of the document to open + /// + /// # Returns + /// An `Option` containing a [`TrackedDocumentView`] if the document was found, or `None` otherwise. + #[must_use] + pub fn open_document(&self, document_id: DocumentId) -> Option { + self.1.track(AccessEvent::OpenDocument(document_id)); + self.0 + .open_document(document_id) + .map(|d| TrackedDocumentView(d, self.1.clone())) + } + + /// Plans the creation of a new empty document. + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Returns + /// The unique identifier of the document recorded to `cb`. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + #[must_use] + pub fn create_document<'b, 'c>( + &'b self, + cb: &'c mut ChangeBuilder, + path: Path, + ) -> PlannedDocument<'b, 'c> { + self.0.create_document(cb, path) + } + + /// Plans the creation of a new empty data section with type `M`. + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Returns + /// + /// The unique identifier of the data recorded to `cb`. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn create_data(&self, cb: &mut ChangeBuilder) -> DataId { + self.0.create_data::(cb) + } + + /// Opens a read only [`TrackedDataView`]. + /// + /// # Arguments + /// * `data_id` - The unique identifier of the document to open + /// + /// # Type Parameters + /// * `M` - The [`Module`] expected to describe the data + /// + /// # Returns + /// An `Option` containing a [`TrackedDataView`] if the document was found and is of type `M`, or `None` otherwise. + #[must_use] + pub fn open_data_by_id(&self, data_id: DataId) -> Option> { + self.1.track(AccessEvent::OpenDataById(data_id)); + self.0 + .open_data_by_id(data_id) + .map(|d| TrackedDataView(d, self.1.clone())) + } + + /// Opens read only [`TrackedDataView`]s to all data with the type `M`. + /// + /// # Type Parameters + /// * `M` - The [`Module`] to filter by + /// + /// # Returns + /// An iterator yielding [`TrackedDataView`]s of type `M` found in this document. + pub fn open_data_by_type(&self) -> impl Iterator> + '_ { + self.1 + .track(AccessEvent::OpenDataByType(ModuleId::from_module::())); + self.0 + .open_data_by_type() + .map(|d| TrackedDataView(d, self.1.clone())) + } +} + +/// A wrapper around [`DocumentView`] that provides access tracking functionality. +#[derive(Clone, Debug)] +pub struct TrackedDocumentView<'a>(DocumentView<'a>, AccessRecorder); + +impl ProjectSource for TrackedDocumentView<'_> { + fn uuid(&self) -> Uuid { + self.0.uuid + } +} + +impl From> for DocumentId { + fn from(dv: TrackedDocumentView<'_>) -> Self { + dv.0.id + } +} + +impl TrackedDocumentView<'_> { + /// Opens a read only [`TrackedDataView`] to data contained in this document. + /// + /// # Arguments + /// * `data_id` - The unique identifier of the document to open + /// + /// # Type Parameters + /// * `M` - The [`Module`] expected to describe the data + /// + /// # Returns + /// An `Option` containing a [`TrackedDataView`] if the document was found in this document and is of type `M`, or `None` otherwise. + #[must_use] + pub fn open_data_by_id(&self, data_id: DataId) -> Option> { + self.1.track(AccessEvent::OpenDocumentDataById( + self.0.clone().into(), + data_id, + )); + self.0 + .open_data_by_id(data_id) + .map(|d| TrackedDataView(d, self.1.clone())) + } + + /// Opens read only [`TrackedDataView`]s to all data with the type `M`. + /// + /// # Type Parameters + /// * `M` - The [`Module`] to filter by + /// + /// # Returns + /// An iterator yielding [`TrackedDataView`]s of type `M` found in this document. + pub fn open_data_by_type(&self) -> impl Iterator> + '_ { + self.1.track(AccessEvent::OpenDocumentDataByType( + self.0.clone().into(), + ModuleId::from_module::(), + )); + self.0 + .open_data_by_type() + .map(|d| TrackedDataView(d, self.1.clone())) + } + + /// Plans the creation of a new empty data section with type `M` + /// + /// The new data section will be contained in this document + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Returns + /// The unique identifier of the data recorded to `cb`. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + #[must_use] + pub fn create_data<'b, 'c, M: Module>( + &'b self, + cb: &'c mut ChangeBuilder, + ) -> PlannedData<'b, 'c, M> { + self.0.create_data(cb) + } + + /// Plans the deletion of this document and all its contained data + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn delete(&self, cb: &mut ChangeBuilder) { + self.0.delete(cb); + } +} + +/// A wrapper around [`DataView`] that provides access tracking functionality. +#[derive(Clone, Debug)] +pub struct TrackedDataView<'a, M: Module>(DataView<'a, M>, AccessRecorder); + +impl ProjectSource for TrackedDataView<'_, M> { + fn uuid(&self) -> Uuid { + self.0.uuid + } +} + +impl From> for DataId { + fn from(dv: TrackedDataView<'_, M>) -> Self { + dv.0.id + } +} + +impl<'a, M: Module> TrackedDataView<'a, M> { + /// Plans to apply a transaction to [`Module::PersistentData`]. + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Arguments + /// + /// * `args` - Arguments of the transaction. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn apply_persistent( + &self, + args: ::Args, + cb: &mut ChangeBuilder, + ) { + self.0.apply_persistent(args, cb); + } + + /// Plans to apply a transaction to [`Module::PersistentUserData`]. + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Arguments + /// + /// * `args` - Arguments of the transaction. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn apply_persistent_user( + &self, + args: ::Args, + cb: &mut ChangeBuilder, + ) { + self.0.apply_persistent_user(args, cb); + } + + /// Plans to apply a transaction to [`Module::SessionData`]. + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Arguments + /// + /// * `args` - Arguments of the transaction. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn apply_session( + &self, + args: ::Args, + cb: &mut ChangeBuilder, + ) { + self.0.apply_session(args, cb); + } + + /// Plans to apply a transaction to [`Module::SharedData`]. + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Arguments + /// + /// * `args` - Arguments of the transaction. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn apply_shared(&self, args: ::Args, cb: &mut ChangeBuilder) { + self.0.apply_shared(args, cb); + } + + /// Plans the deletion of this data + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn delete(&self, cb: &mut ChangeBuilder) { + self.0.delete(cb); + } + + /// Plans to move this data section to another document. + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Arguments + /// + /// * `new_owner` - The document to move the data to. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn move_to_document(&self, new_owner: &crate::TrackedDocumentView, cb: &mut ChangeBuilder) { + self.0.move_to_document(&new_owner.0, cb); + } + + /// Plans to move this data section to another planned document. + /// + /// This will not modify the [`crate::Project`], just record this change to the + /// same [`ChangeBuilder`] [`crate::PlannedDocument`] was created with. + /// + /// # Arguments + /// + /// * `new_owner` - The planned document to move the data to. + /// + /// # Panics + /// If a [`crate::PlannedDocument`] for a different [`crate::Project`] was passed. + pub fn move_to_planned_document(&self, new_owner: &mut crate::PlannedDocument) { + self.0.move_to_planned_document(new_owner); + } + + /// Plans to make this data section an orphan (not owned by any document). + /// + /// This will not modify the [`crate::Project`], just record this change to `cb`. + /// + /// # Panics + /// If a [`ChangeBuilder`] of a different [`crate::Project`] was passed. + pub fn make_orphan(&self, cb: &mut ChangeBuilder) { + self.0.make_orphan(cb); + } + + /// Accesses the persistent data section, shared by all users. + /// + /// # Returns + /// A reference to [`DataView::persistent`]. + #[must_use] + pub fn persistent(&self) -> &'a &::PersistentData { + self.1.track(AccessEvent::AccessPesistent(self.0.id)); + &self.0.persistent + } + + /// Accesses the persistent user-specific data section. + /// + /// # Returns + /// A reference to [`DataView::persistent_user`]. + #[must_use] + pub fn persistent_user(&self) -> &'a &::PersistentUserData { + self.1.track(AccessEvent::AccessPesistentUser(self.0.id)); + &self.0.persistent_user + } + + /// Accesses the non-persistent data section, that is shared among other users. + /// + /// # Returns + /// A reference to [`DataView::shared_data`]. + #[must_use] + pub fn shared_data(&self) -> &'a &::SharedData { + self.1.track(AccessEvent::AccessShared(self.0.id)); + &self.0.shared_data + } + + /// Accesses the non-persistent user-specific data section. + /// + /// # Returns + /// A reference to [`DataView::session_data`]. + #[must_use] + pub fn session_data(&self) -> &'a &::SessionData { + self.1.track(AccessEvent::AccessSession(self.0.id)); + &self.0.session_data + } +} diff --git a/crates/project/tests/thread_safety.rs b/crates/project/tests/thread_safety.rs index b2da5d6..86bd57f 100644 --- a/crates/project/tests/thread_safety.rs +++ b/crates/project/tests/thread_safety.rs @@ -16,4 +16,10 @@ fn test_send_sync() { assert_send_sync::(); assert_send_sync::>(); assert_send_sync::>(); + + assert_send_sync::(); + assert_send_sync::(); + assert_send_sync::(); + assert_send_sync::>(); + assert_send_sync::>(); } diff --git a/crates/project/tests/tracked.rs b/crates/project/tests/tracked.rs new file mode 100644 index 0000000..293d9fa --- /dev/null +++ b/crates/project/tests/tracked.rs @@ -0,0 +1,269 @@ +mod common; +use common::{minimal_test_module::MinimalTestModule, setup_project}; +use project::*; +use std::sync::Arc; + +#[test] +fn test_tracked_view_not_accessed() { + let mut reg = ModuleRegistry::new(); + reg.register::(); + + let mut project = Project::new(); + { + let mut cb = ChangeBuilder::from(&project); + let view = project.create_view(®).unwrap(); + let _ = view.create_data::(&mut cb); + project.apply_changes(cb, ®).unwrap(); + } + + let view = Arc::new(project.create_view(®).unwrap()); + let (_tracked_view, recorder) = TrackedProjectView::new(view); + + let validator = recorder.freeze(); + assert!(!validator.was_accessed()); +} + +#[test] +fn test_tracked_view_basic_access() { + let mut reg = ModuleRegistry::new(); + reg.register::(); + + let mut project = Project::new(); + let data_id; + { + let mut cb = ChangeBuilder::from(&project); + let view = project.create_view(®).unwrap(); + data_id = view.create_data::(&mut cb); + project.apply_changes(cb, ®).unwrap(); + } + + let view = Arc::new(project.create_view(®).unwrap()); + let (tracked_view, recorder) = TrackedProjectView::new(view); + + // Access data to generate tracking events + let _data = tracked_view + .open_data_by_id::(data_id) + .unwrap(); + + let validator = recorder.freeze(); + assert!(validator.was_accessed()); +} + +#[test] +fn test_document_operations() { + let p = setup_project(); + let view = Arc::new(p.view()); + let (tracked_view, recorder) = TrackedProjectView::new(view.clone()); + + // Test document creation + let mut cb = ChangeBuilder::from(&p.project); + let _new_doc = + tracked_view.create_document(&mut cb, Path::new("/new_doc".to_string()).unwrap()); + + // Test document access and data operations within document + let doc = tracked_view.open_document(p.doc1).unwrap(); + let data_by_type: Vec<_> = doc.open_data_by_type::().collect(); + assert!(!data_by_type.is_empty()); + + // Test document data access by ID + let data = doc + .open_data_by_id::(p.doc1_minimal_data) + .unwrap(); + assert_eq!(data.session_data().num, 10); + + let validator = recorder.freeze(); + assert!(validator.was_accessed()); +} + +#[test] +fn test_data_operations() { + let p = setup_project(); + let view = Arc::new(p.view()); + let (tracked_view, recorder) = TrackedProjectView::new(view.clone()); + + // Test different ways to access data + let by_id = tracked_view.open_data_by_id::(p.doc1_minimal_data); + assert!(by_id.is_some()); + + let by_type: Vec<_> = tracked_view + .open_data_by_type::() + .collect(); + assert!(!by_type.is_empty()); + + let validator = recorder.freeze(); + assert!(validator.was_accessed()); +} + +#[test] +fn test_different_project_validation() { + let p1 = setup_project(); + let p2 = setup_project(); + + let view1 = Arc::new(p1.view()); + let view2 = Arc::new(p2.view()); + + let (tracked_view, recorder) = TrackedProjectView::new(view1.clone()); + + // Generate some access events + let _doc = tracked_view.open_document(p1.doc1); + + let validator = recorder.freeze(); + + // Should be invalid for different projects + assert!(!validator.is_cache_valid(&view1, &view2)); +} + +#[test] +fn test_cache_validation_open_data_by_type() { + let mut p = setup_project(); + let view = Arc::new(p.view()); + let (tracked_view, recorder) = TrackedProjectView::new(view.clone()); + + // Access data by type to generate tracking event + let data_list: Vec<_> = tracked_view + .open_data_by_type::() + .collect(); + assert!(!data_list.is_empty()); + + let validator = recorder.freeze(); + assert!(validator.is_cache_valid(&view, &view)); + + // Create new data of same type + let mut cb = ChangeBuilder::from(&view); + tracked_view.create_data::(&mut cb); + p.project.apply_changes(cb, &p.reg).unwrap(); + let new_view = p.view(); + + // Cache should be invalid as the list of data of this type changed + assert!(!validator.is_cache_valid(&view, &new_view)); +} + +#[test] +fn test_cache_validation_document_data_by_id() { + let mut p = setup_project(); + let view = Arc::new(p.view()); + let (tracked_view, recorder) = TrackedProjectView::new(view.clone()); + + // Access document data by ID + let doc = tracked_view.open_document(p.doc1).unwrap(); + let data = doc + .open_data_by_id::(p.doc1_minimal_data) + .unwrap(); + let _session = data.session_data(); // Access some data to ensure it's tracked + + let validator = recorder.freeze(); + assert!(validator.is_cache_valid(&view, &view)); + + // Move data to different document + let mut cb = ChangeBuilder::from(&view); + let mut new_doc = + tracked_view.create_document(&mut cb, Path::new("/new_doc".to_string()).unwrap()); + data.move_to_planned_document(&mut new_doc); + p.project.apply_changes(cb, &p.reg).unwrap(); + let new_view = p.view(); + + // Cache should be invalid as the data moved documents + assert!(!validator.is_cache_valid(&view, &new_view)); +} + +#[test] +fn test_cache_validation_document_data_by_type() { + let mut p = setup_project(); + let view = Arc::new(p.view()); + let (tracked_view, recorder) = TrackedProjectView::new(view.clone()); + + // Access document data by type + let doc = tracked_view.open_document(p.doc1).unwrap(); + let data_list: Vec<_> = doc.open_data_by_type::().collect(); + assert!(!data_list.is_empty()); + + let validator = recorder.freeze(); + assert!(validator.is_cache_valid(&view, &view)); + + // Add new data of same type to document + let mut cb = ChangeBuilder::from(&view); + let _data = doc.create_data::(&mut cb); + p.project.apply_changes(cb, &p.reg).unwrap(); + let new_view = p.view(); + + // Cache should be invalid as document's data list changed + assert!(!validator.is_cache_valid(&view, &new_view)); +} + +#[test] +fn test_cache_validation_complex_scenario() { + let mut p = setup_project(); + let view = Arc::new(p.view()); + let (tracked_view, recorder) = TrackedProjectView::new(view.clone()); + + // Generate multiple types of access events + let doc = tracked_view.open_document(p.doc1).unwrap(); + + // Access by type globally + let _global_data: Vec<_> = tracked_view + .open_data_by_type::() + .collect(); + + // Access by type in document + let _doc_data: Vec<_> = doc.open_data_by_type::().collect(); + + // Access specific data + let data = doc + .open_data_by_id::(p.doc1_minimal_data) + .unwrap(); + let _session = data.session_data(); + + let validator = recorder.freeze(); + assert!(validator.is_cache_valid(&view, &view)); + + // Make various modifications + let mut cb = ChangeBuilder::from(&view); + + // Create new data + let _new_data_id = tracked_view.create_data::(&mut cb); + + // Move existing data + let mut new_doc = + tracked_view.create_document(&mut cb, Path::new("/new_doc".to_string()).unwrap()); + data.move_to_planned_document(&mut new_doc); + + // Modify data content + data.apply_session(42, &mut cb); + + p.project.apply_changes(cb, &p.reg).unwrap(); + let new_view = p.view(); + + // Cache should be invalid due to multiple changes + assert!(!validator.is_cache_valid(&view, &new_view)); +} + +#[test] +fn test_cache_validation_edge_cases() { + // Create two separate projects to get valid but non-existent IDs + let mut p1 = setup_project(); + let p2 = setup_project(); + + let view = Arc::new(p1.view()); + let (tracked_view, recorder) = TrackedProjectView::new(view.clone()); + + // Test with document that exists in p2 but not in p1 + let _missing_doc = tracked_view.open_document(p2.doc1); + + // Test with non-existent data (using data ID from p2) + let doc = tracked_view.open_document(p1.doc1).unwrap(); + let _missing_data = doc.open_data_by_id::(p2.doc1_minimal_data); + + // Test with empty data type list + let _empty_list = doc.open_data_by_type::(); + + let validator = recorder.freeze(); + + // Modify project + let mut cb = ChangeBuilder::from(&view); + let _data = doc.create_data::(&mut cb); + p1.project.apply_changes(cb, &p1.reg).unwrap(); + let new_view = p1.view(); + + // Validate cache with edge cases + assert!(!validator.is_cache_valid(&view, &new_view)); +}