Restore project diff test (#21606)

Kirill Bulatov and Cole Miller created

Restores a basic project diff test

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

crates/editor/src/git/project_diff.rs | 256 +++++++++++++++-------------
crates/project/src/buffer_store.rs    |   7 
2 files changed, 146 insertions(+), 117 deletions(-)

Detailed changes

crates/editor/src/git/project_diff.rs 🔗

@@ -55,6 +55,7 @@ struct ProjectDiffEditor {
     _subscriptions: Vec<Subscription>,
 }
 
+#[derive(Debug)]
 struct Changes {
     _status: GitFileStatus,
     buffer: Model<Buffer>,
@@ -235,15 +236,16 @@ impl ProjectDiffEditor {
                         let mut change_sets = Vec::new();
                         for (status, entry_id, entry_path, open_task) in open_tasks {
                             let (_, opened_model) = open_task.await.with_context(|| {
-                                format!("loading buffer {} for git diff", entry_path.path.display())
+                                format!("loading buffer {:?} for git diff", entry_path.path)
                             })?;
                             let buffer = match opened_model.downcast::<Buffer>() {
                                 Ok(buffer) => buffer,
                                 Err(_model) => anyhow::bail!(
-                                    "Could not load {} as a buffer for git diff",
-                                    entry_path.path.display()
+                                    "Could not load {:?} as a buffer for git diff",
+                                    entry_path.path
                                 ),
                             };
+
                             let change_set = project
                                 .update(&mut cx, |project, cx| {
                                     project.open_unstaged_changes(buffer.clone(), cx)
@@ -1089,13 +1091,16 @@ impl Render for ProjectDiffEditor {
 
 #[cfg(test)]
 mod tests {
-    // use std::{ops::Deref as _, path::Path, sync::Arc};
-
-    // use fs::RealFs;
-    // use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
-    // use settings::SettingsStore;
+    use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
+    use project::buffer_store::BufferChangeSet;
+    use serde_json::json;
+    use settings::SettingsStore;
+    use std::{
+        ops::Deref as _,
+        path::{Path, PathBuf},
+    };
 
-    // use super::*;
+    use super::*;
 
     // TODO finish
     // #[gpui::test]
@@ -1111,114 +1116,131 @@ mod tests {
     //     // Apply randomized changes to the project: select a random file, random change and apply to buffers
     // }
 
-    // #[gpui::test]
-    // async fn simple_edit_test(cx: &mut TestAppContext) {
-    //     cx.executor().allow_parking();
-    //     init_test(cx);
-
-    //     let dir = tempfile::tempdir().unwrap();
-    //     let dst = dir.path();
-
-    //     std::fs::write(dst.join("file_a"), "This is file_a").unwrap();
-    //     std::fs::write(dst.join("file_b"), "This is file_b").unwrap();
-
-    //     run_git(dst, &["init"]);
-    //     run_git(dst, &["add", "*"]);
-    //     run_git(dst, &["commit", "-m", "Initial commit"]);
-
-    //     let project = Project::test(Arc::new(RealFs::default()), [dst], cx).await;
-    //     let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-    //     let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
-
-    //     let file_a_editor = workspace
-    //         .update(cx, |workspace, cx| {
-    //             let file_a_editor = workspace.open_abs_path(dst.join("file_a"), true, cx);
-    //             ProjectDiffEditor::deploy(workspace, &Deploy, cx);
-    //             file_a_editor
-    //         })
-    //         .unwrap()
-    //         .await
-    //         .expect("did not open an item at all")
-    //         .downcast::<Editor>()
-    //         .expect("did not open an editor for file_a");
-
-    //     let project_diff_editor = workspace
-    //         .update(cx, |workspace, cx| {
-    //             workspace
-    //                 .active_pane()
-    //                 .read(cx)
-    //                 .items()
-    //                 .find_map(|item| item.downcast::<ProjectDiffEditor>())
-    //         })
-    //         .unwrap()
-    //         .expect("did not find a ProjectDiffEditor");
-    //     project_diff_editor.update(cx, |project_diff_editor, cx| {
-    //         assert!(
-    //             project_diff_editor.editor.read(cx).text(cx).is_empty(),
-    //             "Should have no changes after opening the diff on no git changes"
-    //         );
-    //     });
-
-    //     let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
-    //     let change = "an edit after git add";
-    //     file_a_editor
-    //         .update(cx, |file_a_editor, cx| {
-    //             file_a_editor.insert(change, cx);
-    //             file_a_editor.save(false, project.clone(), cx)
-    //         })
-    //         .await
-    //         .expect("failed to save a file");
-    //     cx.executor().advance_clock(Duration::from_secs(1));
-    //     cx.run_until_parked();
-
-    //     // TODO does not work on Linux for some reason, returning a blank line
-    //     // hence disable the last check for now, and do some fiddling to avoid the warnings.
-    //     #[cfg(target_os = "linux")]
-    //     {
-    //         if true {
-    //             return;
-    //         }
-    //     }
-    //     project_diff_editor.update(cx, |project_diff_editor, cx| {
-    //         // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
-    //         assert_eq!(
-    //             project_diff_editor.editor.read(cx).text(cx),
-    //             format!("{change}{old_text}"),
-    //             "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
-    //         );
-    //     });
-    // }
+    #[gpui::test(iterations = 30)]
+    async fn simple_edit_test(cx: &mut TestAppContext) {
+        cx.executor().allow_parking();
+        init_test(cx);
+
+        let fs = fs::FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root",
+            json!({
+                ".git": {},
+                "file_a": "This is file_a",
+                "file_b": "This is file_b",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), [Path::new("/root")], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+
+        let file_a_editor = workspace
+            .update(cx, |workspace, cx| {
+                let file_a_editor =
+                    workspace.open_abs_path(PathBuf::from("/root/file_a"), true, cx);
+                ProjectDiffEditor::deploy(workspace, &Deploy, cx);
+                file_a_editor
+            })
+            .unwrap()
+            .await
+            .expect("did not open an item at all")
+            .downcast::<Editor>()
+            .expect("did not open an editor for file_a");
+        let project_diff_editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace
+                    .active_pane()
+                    .read(cx)
+                    .items()
+                    .find_map(|item| item.downcast::<ProjectDiffEditor>())
+            })
+            .unwrap()
+            .expect("did not find a ProjectDiffEditor");
+        project_diff_editor.update(cx, |project_diff_editor, cx| {
+            assert!(
+                project_diff_editor.editor.read(cx).text(cx).is_empty(),
+                "Should have no changes after opening the diff on no git changes"
+            );
+        });
 
-    // fn run_git(path: &Path, args: &[&str]) -> String {
-    //     let output = std::process::Command::new("git")
-    //         .args(args)
-    //         .current_dir(path)
-    //         .output()
-    //         .expect("git commit failed");
-
-    //     format!(
-    //         "Stdout: {}; stderr: {}",
-    //         String::from_utf8(output.stdout).unwrap(),
-    //         String::from_utf8(output.stderr).unwrap()
-    //     )
-    // }
+        let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
+        let change = "an edit after git add";
+        file_a_editor
+            .update(cx, |file_a_editor, cx| {
+                file_a_editor.insert(change, cx);
+                file_a_editor.save(false, project.clone(), cx)
+            })
+            .await
+            .expect("failed to save a file");
+        file_a_editor.update(cx, |file_a_editor, cx| {
+            let change_set = cx.new_model(|cx| {
+                BufferChangeSet::new_with_base_text(
+                    old_text.clone(),
+                    file_a_editor
+                        .buffer()
+                        .read(cx)
+                        .as_singleton()
+                        .unwrap()
+                        .read(cx)
+                        .text_snapshot(),
+                    cx,
+                )
+            });
+            file_a_editor
+                .diff_map
+                .add_change_set(change_set.clone(), cx);
+            project.update(cx, |project, cx| {
+                project.buffer_store().update(cx, |buffer_store, cx| {
+                    buffer_store.set_change_set(
+                        file_a_editor
+                            .buffer()
+                            .read(cx)
+                            .as_singleton()
+                            .unwrap()
+                            .read(cx)
+                            .remote_id(),
+                        change_set,
+                    );
+                });
+            });
+        });
+        fs.set_status_for_repo_via_git_operation(
+            Path::new("/root/.git"),
+            &[(Path::new("file_a"), GitFileStatus::Modified)],
+        );
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+        cx.run_until_parked();
+
+        project_diff_editor.update(cx, |project_diff_editor, cx| {
+            assert_eq!(
+                // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
+                project_diff_editor.editor.read(cx).text(cx),
+                format!("{change}{old_text}"),
+                "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
+            );
+        });
+    }
 
-    // fn init_test(cx: &mut gpui::TestAppContext) {
-    //     if std::env::var("RUST_LOG").is_ok() {
-    //         env_logger::try_init().ok();
-    //     }
-
-    //     cx.update(|cx| {
-    //         assets::Assets.load_test_fonts(cx);
-    //         let settings_store = SettingsStore::test(cx);
-    //         cx.set_global(settings_store);
-    //         theme::init(theme::LoadThemes::JustBase, cx);
-    //         release_channel::init(SemanticVersion::default(), cx);
-    //         client::init_settings(cx);
-    //         language::init(cx);
-    //         Project::init_settings(cx);
-    //         workspace::init_settings(cx);
-    //         crate::init(cx);
-    //     });
-    // }
+    fn init_test(cx: &mut gpui::TestAppContext) {
+        if std::env::var("RUST_LOG").is_ok() {
+            env_logger::try_init().ok();
+        }
+
+        cx.update(|cx| {
+            assets::Assets.load_test_fonts(cx);
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            theme::init(theme::LoadThemes::JustBase, cx);
+            release_channel::init(SemanticVersion::default(), cx);
+            client::init_settings(cx);
+            language::init(cx);
+            Project::init_settings(cx);
+            workspace::init_settings(cx);
+            crate::init(cx);
+            cx.set_staff(true);
+        });
+    }
 }

crates/project/src/buffer_store.rs 🔗

@@ -49,6 +49,7 @@ struct SharedBuffer {
     unstaged_changes: Option<Model<BufferChangeSet>>,
 }
 
+#[derive(Debug)]
 pub struct BufferChangeSet {
     pub buffer_id: BufferId,
     pub base_text: Option<Model<Buffer>>,
@@ -1045,6 +1046,12 @@ impl BufferStore {
             .spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn set_change_set(&mut self, buffer_id: BufferId, change_set: Model<BufferChangeSet>) {
+        self.loading_change_sets
+            .insert(buffer_id, Task::ready(Ok(change_set)).shared());
+    }
+
     pub async fn open_unstaged_changes_internal(
         this: WeakModel<Self>,
         text: Result<Option<String>>,