From b7e56c480adb2e554987865b2f2fac0b00dbadf6 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:32:52 +0800 Subject: [PATCH] test(ui): run the window and pane gpui tests on Windows too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gpui test modules were gated `#[cfg(all(test, unix))]` for one reason, written down in three of their own doc comments: the pane harnesses handed back a `std::os::unix::net::UnixStream`. That reason went away when `quiet_test_pane` grew a loopback fallback (#736) and nobody came back for the gates, so 180 window, pane, switcher, file tree and SCM tests silently did not run on the platform tty7 ships on. Give the pair its own helper, `terminal::view::test_stream_pair`, and let the harnesses hand back `daemon::transport::Stream` — a socketpair on Unix, a loopback pair on Windows, which is what `quiet_test_pane` already returned. Everything downstream then only names the portable type, and the gates come off. `--bin tty7-app` on Windows goes from 1468 tests to 1638. What stays gated stays for a reason now named at the gate rather than assumed: `/etc/hosts` is not an absolute path on Windows, so a pane's link probe never wants it; and `git rev-parse --show-toplevel` prints `C:/Users/…` while the OS spells the same directory `C:\Users\…`, so the SCM panel, the commit detail and the diff overlay never recognise the repository they are already showing. Both are product divergences, not test ones, and neither is fixed here. --- src/terminal/view.rs | 81 ++++++++++++++++++++++++++++-------------- src/ui/app.rs | 19 ++++------ src/ui/diff_overlay.rs | 12 +++++-- src/ui/file_tree.rs | 6 ++-- src/ui/scm/detail.rs | 12 ++++++- src/ui/scm/graph.rs | 6 +--- src/ui/scm/panel.rs | 10 ++++-- src/ui/switcher.rs | 2 +- src/ui/tree_sync.rs | 2 -- 9 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/terminal/view.rs b/src/terminal/view.rs index 316d10cd..41249aad 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -98,7 +98,7 @@ pub fn declare_displayed(cx: &App, panes: impl IntoIterator Option { cx.try_global::()? .0 @@ -8848,34 +8848,54 @@ mod tests { } } +/// A connected pair of [`crate::daemon::transport::Stream`]s, one for each end +/// of a pane's link to its daemon. +/// +/// The client half is what a pane really reads and writes; the daemon half is +/// the test's, to speak protocol into. +/// +/// This is the one thing a pane harness needs that Unix and Windows spell +/// differently — `socketpair` there, a loopback connect here — and every gpui +/// test in this crate is portable once it goes through this instead of naming +/// `UnixStream` itself. +#[cfg(test)] +pub(crate) fn test_stream_pair() -> ( + crate::daemon::transport::Stream, + crate::daemon::transport::Stream, +) { + #[cfg(unix)] + { + std::os::unix::net::UnixStream::pair().unwrap() + } + #[cfg(windows)] + { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let client_side = std::net::TcpStream::connect(addr).unwrap(); + let (daemon_side, _) = listener.accept().unwrap(); + (client_side, daemon_side) + } +} + #[cfg(test)] pub(crate) fn quiet_test_pane( pane_id: u64, window: &mut Window, cx: &mut gpui::App, ) -> (gpui::Entity, crate::daemon::transport::Stream) { - #[cfg(unix)] - let (client_side, daemon_side) = std::os::unix::net::UnixStream::pair().unwrap(); - #[cfg(windows)] - let (client_side, daemon_side) = { - let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); - let client_side = std::net::TcpStream::connect(addr).unwrap(); - let (daemon_side, _) = listener.accept().unwrap(); - (client_side, daemon_side) - }; + let (client_side, daemon_side) = test_stream_pair(); let terminal = RemoteTerminal::from_stream(client_side, TermSize::new(80, 24)) .expect("quiet test terminal"); let view = cx.new(|cx| TerminalView::with_terminal(terminal, pane_id, window, cx)); (view, daemon_side) } -#[cfg(all(test, unix))] +#[cfg(test)] pub(crate) fn quiet_test_ssh_pane( pane_id: u64, window: &mut Window, cx: &mut gpui::App, -) -> (gpui::Entity, std::os::unix::net::UnixStream) { +) -> (gpui::Entity, crate::daemon::transport::Stream) { let (view, stream) = quiet_test_pane(pane_id, window, cx); view.update(cx, |view, _| { view.ssh_spec = Some(Box::new( @@ -8888,20 +8908,20 @@ pub(crate) fn quiet_test_ssh_pane( (view, stream) } -#[cfg(all(test, unix))] +#[cfg(test)] mod gpui_tests { use super::*; use crate::daemon::protocol::{ClientMsg, DaemonMsg}; + use crate::daemon::transport::Stream; use gpui::{Entity, TestAppContext, point}; - use std::os::unix::net::UnixStream; - fn harness(cx: &mut TestAppContext) -> (gpui::WindowHandle, UnixStream) { + fn harness(cx: &mut TestAppContext) -> (gpui::WindowHandle, Stream) { // Building a view reads the config. Whether that hit the real user // directory used to come down to which test happened to pin the // scratch dir first. crate::core::config::pin_test_config_dir(); cx.executor().allow_parking(); - let (client_side, daemon_side) = UnixStream::pair().unwrap(); + let (client_side, daemon_side) = super::test_stream_pair(); cx.update(|cx| { gpui_component::init(cx); cx.set_global(Config::default()); @@ -8926,11 +8946,11 @@ mod gpui_tests { ) -> ( gpui::WindowHandle, Entity, - UnixStream, + Stream, ) { crate::core::config::pin_test_config_dir(); cx.executor().allow_parking(); - let (client_side, daemon_side) = UnixStream::pair().unwrap(); + let (client_side, daemon_side) = super::test_stream_pair(); cx.update(|cx| { gpui_component::init(cx); cx.set_global(Config::default()); @@ -8956,7 +8976,7 @@ mod gpui_tests { fn prompt_ready( window: &gpui::WindowHandle, cx: &mut TestAppContext, - daemon: &mut UnixStream, + daemon: &mut Stream, ) { DaemonMsg::Prompt { active: true, @@ -8980,7 +9000,7 @@ mod gpui_tests { fn alt_screen_ready( window: &gpui::WindowHandle, cx: &mut TestAppContext, - daemon: &mut UnixStream, + daemon: &mut Stream, ) { DaemonMsg::Output(b"\x1b[?1049h".to_vec()) .encode(daemon) @@ -9008,7 +9028,7 @@ mod gpui_tests { .encode(&mut daemon) .unwrap(); - let report = |status: AgentStatus, daemon: &mut UnixStream| { + let report = |status: AgentStatus, daemon: &mut Stream| { DaemonMsg::AgentStatus(Some(AgentSessionState { status, message: None, @@ -9115,7 +9135,7 @@ mod gpui_tests { status: crate::core::cli_agent::AgentStatus, pane: &gpui::Entity, cx: &mut TestAppContext, - daemon: &mut UnixStream, + daemon: &mut Stream, ) { use crate::core::cli_agent::AgentSessionState; @@ -9565,6 +9585,15 @@ mod gpui_tests { /// must not have made the promise. Otherwise those paths sit "not answered /// yet" for the life of the pane — no underline, and a click that says /// nothing, which is the silence this whole path exists to remove. + /// + /// Unix-only because the path it prints is: `Path::new("/etc/hosts")` + /// is not absolute on Windows, so `FileCandidate::paths` measures it + /// from the roots rather than letting it stand alone — and a workspace + /// that never connected has no roots, so nothing is ever wanted. Which + /// is itself the divergence: a Windows tty7 looking at a *remote* Linux + /// pane never probes the POSIX paths that pane prints, and so never + /// underlines them. + #[cfg(unix)] #[gpui::test] fn a_probe_with_no_host_to_ask_stays_wanted(cx: &mut TestAppContext) { let (window, mut daemon) = harness(cx); @@ -9756,7 +9785,7 @@ mod gpui_tests { .unwrap(); } - fn next_input(daemon: &mut UnixStream) -> Vec { + fn next_input(daemon: &mut Stream) -> Vec { loop { match ClientMsg::read(daemon).expect("client socket stays open") { ClientMsg::Input(bytes) => return bytes, @@ -9788,7 +9817,7 @@ mod gpui_tests { } } - fn next_input_until_timeout(daemon: &mut UnixStream) -> Option> { + fn next_input_until_timeout(daemon: &mut Stream) -> Option> { use std::io::ErrorKind; daemon @@ -12533,7 +12562,7 @@ mod gpui_tests { } assert_eq!(seen, "before", "the pre-drop screen is what we relink over"); - let (new_client, mut new_daemon) = UnixStream::pair().unwrap(); + let (new_client, mut new_daemon) = super::test_stream_pair(); window .update(cx, |view, _, cx| { view.adopt_relink( diff --git a/src/ui/app.rs b/src/ui/app.rs index dea55890..ab6dad2b 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -9282,14 +9282,13 @@ pub(crate) mod test_window { } /// A window carrying `n` quiet tabs, active on the first. - #[cfg(unix)] pub(crate) fn harness_with_tabs( cx: &mut TestAppContext, n: usize, ) -> ( Entity, VisualTestContext, - Vec, + Vec, ) { use crate::terminal::view::quiet_test_pane; use crate::ui::pane::{Pane, PaneSlot}; @@ -9316,13 +9315,12 @@ pub(crate) mod test_window { (app, vcx, streams) } - #[cfg(unix)] pub(crate) fn harness_with_pane( cx: &mut TestAppContext, ) -> ( Entity, VisualTestContext, - std::os::unix::net::UnixStream, + crate::daemon::transport::Stream, ) { use crate::terminal::view::quiet_test_pane; use crate::ui::pane::{Pane, PaneSlot}; @@ -9371,7 +9369,6 @@ pub(crate) mod test_window { /// another frame 250ms later. So the sleep below is load-bearing too, and /// a round that drew nothing is not on its own enough to stop on — a burst /// still open is a frame already owed. - #[cfg(unix)] pub(crate) fn quiesce(vcx: &mut VisualTestContext, cwd: Option<&std::path::Path>) { use crate::terminal::git_data::ScmData; use crate::terminal::git_status::GitStatusCache; @@ -9483,7 +9480,7 @@ mod cursor_blink_gpui_tests { } } -#[cfg(all(test, unix))] +#[cfg(test)] mod ssh_rebuild_gpui_tests { use super::test_window::harness_with_pane; use crate::core::session::{ @@ -9920,9 +9917,7 @@ mod shell_menu_gpui_tests { } } -// `harness_with_tabs` hands back the panes' `UnixStream`s, so it exists only -// on unix — same as `ssh_rebuild_gpui_tests` below it. -#[cfg(all(test, unix))] +#[cfg(test)] mod rename_gpui_tests { use gpui::TestAppContext; @@ -10006,7 +10001,7 @@ mod rename_gpui_tests { // everything else hidden; a pane nobody has declared — or whose id nobody // registered — must err toward displayed, because the failure direction that // matters is a visible pane that stops repainting. -#[cfg(all(test, unix))] +#[cfg(test)] mod displayed_gpui_tests { use gpui::TestAppContext; @@ -10107,7 +10102,7 @@ mod displayed_gpui_tests { // Zoom is a tab's view state: it rides with the tab across a switch, while a // layout change (drag, split, close) still clears it. -#[cfg(all(test, unix))] +#[cfg(test)] mod zoom_gpui_tests { use gpui::TestAppContext; @@ -10155,7 +10150,7 @@ mod zoom_gpui_tests { // test config dir and nothing is listening on it — so every forward request // fails. That is exactly the case these are about: what the panel and the form // are left holding when the far side does not answer. -#[cfg(all(test, unix))] +#[cfg(test)] mod managed_forward_gpui_tests { use gpui::TestAppContext; use gpui_component::input::InputState; diff --git a/src/ui/diff_overlay.rs b/src/ui/diff_overlay.rs index 2e869e31..016d3fd7 100644 --- a/src/ui/diff_overlay.rs +++ b/src/ui/diff_overlay.rs @@ -2191,7 +2191,7 @@ mod tests { } } -#[cfg(all(test, unix))] +#[cfg(test)] mod overlay_gpui_tests { use super::*; use crate::ui::app::test_window; @@ -2469,8 +2469,14 @@ mod overlay_gpui_tests { /// terminal, an editor, a worktree command — and the cached branch is a branch /// the repository has left. That is what the stale entry below stands for. /// -/// Unix-gated like every other window harness in this tree: `harness_with_tabs` -/// hands back a `std::os::unix::net::UnixStream` for the pane. +/// Unix-only, and not for the harness: on Windows the repository root git +/// reports (`C:/Users/—`, forward slashes, straight out of MSYS2 git) is +/// not the root the seeded cache below holds, so `scm_epoch` never agrees +/// with the landing snapshot and the overlay re-probes on every frame — +/// `load` reaches `Ready` and `loading` goes straight back to `true`, +/// which is the exact spin this test exists to catch. That is a real +/// divergence in the SCM layer's path comparisons rather than a test +/// artefact, so the gate stays until those roots are compared normalised. #[cfg(all(test, unix))] mod render_idle_gpui_tests { use super::*; diff --git a/src/ui/file_tree.rs b/src/ui/file_tree.rs index f5408ec8..0d379823 100644 --- a/src/ui/file_tree.rs +++ b/src/ui/file_tree.rs @@ -2931,7 +2931,7 @@ mod tests { } } -#[cfg(all(test, unix))] +#[cfg(test)] mod render_idle_gpui_tests { use super::*; use crate::daemon::protocol::DaemonMsg; @@ -2959,7 +2959,7 @@ mod render_idle_gpui_tests { ) -> ( Entity, VisualTestContext, - std::os::unix::net::UnixStream, + crate::daemon::transport::Stream, ) { let (app, mut vcx, mut pane) = test_window::harness_with_pane(cx); DaemonMsg::Cwd(root.to_path_buf()) @@ -3558,7 +3558,7 @@ mod render_idle_gpui_tests { /// What these cannot reach is the hit test — whether the row under the cursor /// is the one that gets the drop is decided by gpui's hitbox stack, and there /// is no headless way to put a cursor over a row. -#[cfg(all(test, unix))] +#[cfg(test)] mod drop_gpui_tests { use super::render_idle_gpui_tests::{files_panel_on, rows, scratch, serial, settle}; use super::*; diff --git a/src/ui/scm/detail.rs b/src/ui/scm/detail.rs index d1ffc073..16c27337 100644 --- a/src/ui/scm/detail.rs +++ b/src/ui/scm/detail.rs @@ -974,6 +974,16 @@ mod tests { /// here — a missing global, a theme token, a slice through the middle of a /// character — goes wrong during layout and paint, so these arm the render /// probe and insist something was actually drawn. +/// +/// Still unix-only, and for a reason worth naming rather than a harness +/// one: on Windows `git rev-parse --show-toplevel` (Git for Windows is +/// MSYS2) prints `C:/Users/—` with forward slashes, and that string is +/// what `tty7_core::core::git::probe` stores as `RepoSnapshot::root`. +/// Every comparison here — and in the SCM cache — is a plain `PathBuf` +/// equality against a path the OS spelled `C:\Users\`, so the two never +/// match and the panel never settles on the directory it is already +/// showing. Taking the gate off needs those roots compared normalised, +/// the way `ui::path_display` already normalises for display. #[cfg(all(test, unix))] mod detail_gpui_tests { use super::*; @@ -1043,7 +1053,7 @@ mod detail_gpui_tests { ) -> ( Entity, VisualTestContext, - std::os::unix::net::UnixStream, + crate::daemon::transport::Stream, ) { let (app, mut vcx, mut pane) = test_window::harness_with_pane(cx); DaemonMsg::Cwd(root.to_path_buf()) diff --git a/src/ui/scm/graph.rs b/src/ui/scm/graph.rs index 553c9a61..99cdcb32 100644 --- a/src/ui/scm/graph.rs +++ b/src/ui/scm/graph.rs @@ -2186,11 +2186,7 @@ mod tests { /// way to know is to settle the window and count frames. Same shape as the file /// tree's own idle tests, including the serial lock: the render probe is /// thread-local and two of these at once would count each other's frames. -/// -/// `unix` for the same reason `panel.rs`, `detail.rs` and `file_tree.rs` gate -/// theirs: a real pane means `test_window::harness_with_pane`, and that harness -/// hands back a `std::os::unix::net::UnixStream`. -#[cfg(all(test, unix))] +#[cfg(test)] mod render_idle_gpui_tests { use super::*; use crate::ui::app::{render_probe, test_window}; diff --git a/src/ui/scm/panel.rs b/src/ui/scm/panel.rs index bf3dc850..abb15b97 100644 --- a/src/ui/scm/panel.rs +++ b/src/ui/scm/panel.rs @@ -2888,7 +2888,7 @@ mod tests { /// `default_global`, which fires the global observers whether or not anything /// changed, and it is called every frame. A watcher that notified on every one /// of those would request a frame from inside a frame and never stop. -#[cfg(all(test, unix))] +#[cfg(test)] mod render_idle_gpui_tests { use super::*; use crate::daemon::protocol::DaemonMsg; @@ -2927,7 +2927,7 @@ mod render_idle_gpui_tests { ) -> ( Entity, VisualTestContext, - std::os::unix::net::UnixStream, + crate::daemon::transport::Stream, ) { let (app, mut vcx, mut pane) = test_window::harness_with_pane(cx); DaemonMsg::Cwd(root.to_path_buf()) @@ -2974,6 +2974,12 @@ mod render_idle_gpui_tests { render_probe::draws() } + /// The only test in this module that waits on `repo.root`, and so the + /// only one Windows cannot run: git spells that root `C:/Users/—` and + /// the pane's own cwd is spelled `C:\Users\`, so the equality below + /// never holds there. Its sibling keys off the pane's cwd instead, and + /// runs everywhere. + #[cfg(unix)] #[gpui::test] fn a_settled_source_control_panel_reaches_render_idle(cx: &mut TestAppContext) { let _serial = serial(); diff --git a/src/ui/switcher.rs b/src/ui/switcher.rs index 2468a40c..b69a2349 100644 --- a/src/ui/switcher.rs +++ b/src/ui/switcher.rs @@ -3687,7 +3687,7 @@ mod tests { } } -#[cfg(all(test, unix))] +#[cfg(test)] mod gpui_tests { use gpui::{Modifiers, TestAppContext}; diff --git a/src/ui/tree_sync.rs b/src/ui/tree_sync.rs index 5ea14edc..ba193b4e 100644 --- a/src/ui/tree_sync.rs +++ b/src/ui/tree_sync.rs @@ -3686,7 +3686,6 @@ mod tests { /// the window shows one, and the one it could not put up must not come out /// of a `Full` diff as `TabClose` — that op deleted from the machine exactly /// the tabs a restart had failed to bring back, panes and all (#672). - #[cfg(unix)] #[gpui::test] fn the_next_sync_leaves_a_tab_the_rebuild_could_not_put_up_on_the_machine( cx: &mut gpui::TestAppContext, @@ -3775,7 +3774,6 @@ mod tests { /// straight after clears the queue, so by the time a test can look the /// ops are gone either way — while `informed` outliving the arrival is /// both durable and the thing that made them possible. - #[cfg(unix)] #[gpui::test] fn arriving_at_a_workspace_does_not_prune_what_is_already_in_it(cx: &mut gpui::TestAppContext) { let (app, mut vcx, _pane_stream) = crate::ui::app::test_window::harness_with_pane(cx);