windows: Get more tests passing (#39984)

Jakub Konka and John Tur created

Still got one more test in `project_tests.rs` to investigate...

Release Notes:

- N/A

---------

Co-authored-by: John Tur <john-tur@outlook.com>

Change summary

crates/editor/src/test/editor_test_context.rs    | 128 ++++++++++++++++-
crates/git_ui/src/project_diff.rs                |  15 -
crates/project/src/project_tests.rs              |   7 
crates/project_panel/src/project_panel_tests.rs  |   6 
crates/remote_server/src/remote_editing_tests.rs |   2 
5 files changed, 138 insertions(+), 20 deletions(-)

Detailed changes

crates/editor/src/test/editor_test_context.rs ๐Ÿ”—

@@ -1,5 +1,5 @@
 use crate::{
-    AnchorRangeExt, DisplayPoint, Editor, MultiBuffer, RowExt,
+    AnchorRangeExt, DisplayPoint, Editor, ExcerptId, MultiBuffer, MultiBufferSnapshot, RowExt,
     display_map::{HighlightKey, ToDisplayPoint},
 };
 use buffer_diff::DiffHunkStatusKind;
@@ -24,6 +24,7 @@ use std::{
         atomic::{AtomicUsize, Ordering},
     },
 };
+use text::Selection;
 use util::{
     assert_set_eq,
     test::{generate_marked_text, marked_text_ranges},
@@ -388,6 +389,23 @@ impl EditorTestContext {
 
     #[track_caller]
     pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
+        let actual_text = self.to_format_multibuffer_as_marked_text();
+        let fmt_additional_notes = || {
+            struct Format<'a, T: std::fmt::Display>(&'a str, &'a T);
+
+            impl<T: std::fmt::Display> std::fmt::Display for Format<'_, T> {
+                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                    write!(
+                        f,
+                        "\n\n----- EXPECTED: -----\n\n{}\n\n----- ACTUAL: -----\n\n{}\n\n",
+                        self.0, self.1
+                    )
+                }
+            }
+
+            Format(marked_text, &actual_text)
+        };
+
         let expected_excerpts = marked_text
             .strip_prefix("[EXCERPT]\n")
             .unwrap()
@@ -408,9 +426,10 @@ impl EditorTestContext {
 
         assert!(
             excerpts.len() == expected_excerpts.len(),
-            "should have {} excerpts, got {}",
+            "should have {} excerpts, got {}{}",
             expected_excerpts.len(),
-            excerpts.len()
+            excerpts.len(),
+            fmt_additional_notes(),
         );
 
         for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
@@ -424,18 +443,25 @@ impl EditorTestContext {
                 if !expected_selections.is_empty() {
                     assert!(
                         is_selected,
-                        "excerpt {ix} should be selected. got {:?}",
+                        "excerpt {ix} should contain selections. got {:?}{}",
                         self.editor_state(),
+                        fmt_additional_notes(),
                     );
                 } else {
                     assert!(
                         !is_selected,
-                        "excerpt {ix} should not be selected, got: {selections:?}",
+                        "excerpt {ix} should not contain selections, got: {selections:?}{}",
+                        fmt_additional_notes(),
                     );
                 }
                 continue;
             }
-            assert!(!is_folded, "excerpt {} should not be folded", ix);
+            assert!(
+                !is_folded,
+                "excerpt {} should not be folded{}",
+                ix,
+                fmt_additional_notes()
+            );
             assert_eq!(
                 multibuffer_snapshot
                     .text_for_range(Anchor::range_in_buffer(
@@ -444,7 +470,9 @@ impl EditorTestContext {
                         range.context.clone()
                     ))
                     .collect::<String>(),
-                expected_text
+                expected_text,
+                "{}",
+                fmt_additional_notes(),
             );
 
             let selections = selections
@@ -460,13 +488,38 @@ impl EditorTestContext {
                 .collect::<Vec<_>>();
             // todo: selections that cross excerpt boundaries..
             assert_eq!(
-                selections, expected_selections,
-                "excerpt {} has incorrect selections",
+                selections,
+                expected_selections,
+                "excerpt {} has incorrect selections{}",
                 ix,
+                fmt_additional_notes()
             );
         }
     }
 
+    fn to_format_multibuffer_as_marked_text(&mut self) -> FormatMultiBufferAsMarkedText {
+        let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
+            let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+
+            let selections = editor.selections.disjoint_anchors_arc().to_vec();
+            let excerpts = multibuffer_snapshot
+                .excerpts()
+                .map(|(e_id, snapshot, range)| {
+                    let is_folded = editor.is_buffer_folded(snapshot.remote_id(), cx);
+                    (e_id, snapshot.clone(), range, is_folded)
+                })
+                .collect::<Vec<_>>();
+
+            (multibuffer_snapshot, selections, excerpts)
+        });
+
+        FormatMultiBufferAsMarkedText {
+            multibuffer_snapshot,
+            selections,
+            excerpts,
+        }
+    }
+
     /// Make an assertion about the editor's text and the ranges and directions
     /// of its selections using a string containing embedded range markers.
     ///
@@ -571,6 +624,63 @@ impl EditorTestContext {
     }
 }
 
+struct FormatMultiBufferAsMarkedText {
+    multibuffer_snapshot: MultiBufferSnapshot,
+    selections: Vec<Selection<Anchor>>,
+    excerpts: Vec<(ExcerptId, BufferSnapshot, ExcerptRange<text::Anchor>, bool)>,
+}
+
+impl std::fmt::Display for FormatMultiBufferAsMarkedText {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let Self {
+            multibuffer_snapshot,
+            selections,
+            excerpts,
+        } = self;
+
+        for (excerpt_id, snapshot, range, is_folded) in excerpts.into_iter() {
+            write!(f, "[EXCERPT]\n")?;
+            if *is_folded {
+                write!(f, "[FOLDED]\n")?;
+            }
+
+            let mut text = multibuffer_snapshot
+                .text_for_range(Anchor::range_in_buffer(
+                    *excerpt_id,
+                    snapshot.remote_id(),
+                    range.context.clone(),
+                ))
+                .collect::<String>();
+
+            let selections = selections
+                .iter()
+                .filter(|&s| s.head().excerpt_id == *excerpt_id)
+                .map(|s| {
+                    let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
+                        - text::ToOffset::to_offset(&range.context.start, &snapshot);
+                    let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
+                        - text::ToOffset::to_offset(&range.context.start, &snapshot);
+                    tail..head
+                })
+                .rev()
+                .collect::<Vec<_>>();
+
+            for selection in selections {
+                if selection.is_empty() {
+                    text.insert(selection.start, 'ห‡');
+                    continue;
+                }
+                text.insert(selection.end, 'ยป');
+                text.insert(selection.start, 'ยซ');
+            }
+
+            write!(f, "{text}")?;
+        }
+
+        Ok(())
+    }
+}
+
 #[track_caller]
 pub fn assert_state_with_diff(
     editor: &Entity<Editor>,

crates/git_ui/src/project_diff.rs ๐Ÿ”—

@@ -1619,14 +1619,13 @@ mod tests {
         project_diff::{self, ProjectDiff},
     };
 
-    #[cfg_attr(windows, ignore = "currently fails on windows")]
     #[gpui::test]
     async fn test_go_to_prev_hunk_multibuffer(cx: &mut TestAppContext) {
         init_test(cx);
 
         let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
-            "/a",
+            path!("/a"),
             json!({
                 ".git": {},
                 "a.txt": "created\n",
@@ -1637,7 +1636,7 @@ mod tests {
         .await;
 
         fs.set_head_and_index_for_repo(
-            Path::new("/a/.git"),
+            Path::new(path!("/a/.git")),
             &[
                 ("b.txt", "before\n".to_string()),
                 ("c.txt", "unchanged\n".to_string()),
@@ -1645,7 +1644,7 @@ mod tests {
             ],
         );
 
-        let project = Project::test(fs, [Path::new("/a")], cx).await;
+        let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 
@@ -1707,7 +1706,6 @@ mod tests {
         ));
     }
 
-    #[cfg_attr(windows, ignore = "currently fails on windows")]
     #[gpui::test]
     async fn test_excerpts_splitting_after_restoring_the_middle_excerpt(cx: &mut TestAppContext) {
         init_test(cx);
@@ -1747,7 +1745,7 @@ mod tests {
 
         let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
-            "/a",
+            path!("/a"),
             json!({
                 ".git": {},
                 "main.rs": buffer_contents,
@@ -1756,11 +1754,11 @@ mod tests {
         .await;
 
         fs.set_head_and_index_for_repo(
-            Path::new("/a/.git"),
+            Path::new(path!("/a/.git")),
             &[("main.rs", git_contents.to_owned())],
         );
 
-        let project = Project::test(fs, [Path::new("/a")], cx).await;
+        let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
 
@@ -1925,6 +1923,7 @@ mod tests {
         cx.run_until_parked();
 
         let editor = diff.read_with(cx, |diff, _| diff.editor.clone());
+
         assert_state_with_diff(
             &editor,
             cx,

crates/project/src/project_tests.rs ๐Ÿ”—

@@ -94,6 +94,9 @@ async fn test_block_via_smol(cx: &mut gpui::TestAppContext) {
     task.await;
 }
 
+// NOTE:
+// While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
+// we assume that they are not supported out of the box.
 #[cfg(not(windows))]
 #[gpui::test]
 async fn test_symlinks(cx: &mut gpui::TestAppContext) {
@@ -8536,6 +8539,7 @@ async fn test_update_gitignore(cx: &mut gpui::TestAppContext) {
 // a directory which some program has already open.
 // This is a limitation of the Windows.
 // See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
+// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
 #[gpui::test]
 #[cfg_attr(target_os = "windows", ignore)]
 async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
@@ -8615,7 +8619,8 @@ async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
 // NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
 // you can't rename a directory which some program has already open. This is a
 // limitation of the Windows. See:
-// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
+// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
+// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
 #[gpui::test]
 #[cfg_attr(target_os = "windows", ignore)]
 async fn test_file_status(cx: &mut gpui::TestAppContext) {

crates/project_panel/src/project_panel_tests.rs ๐Ÿ”—

@@ -2983,6 +2983,12 @@ async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
     );
 }
 
+// NOTE: This test is skipped on Windows, because on Windows, unlike on Unix,
+// you can't rename a directory which some program has already open. This is a
+// limitation of the Windows. Since Zed will have the root open, it will hold an open handle
+// to it, and thus renaming it will fail on Windows.
+// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
+// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
 #[gpui::test]
 #[cfg_attr(target_os = "windows", ignore)]
 async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {

crates/remote_server/src/remote_editing_tests.rs ๐Ÿ”—

@@ -1327,8 +1327,6 @@ async fn test_copy_file_into_remote_project(
     );
 }
 
-// TODO: this test fails on Windows.
-#[cfg(not(windows))]
 #[gpui::test]
 async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
     let text_2 = "