mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-11 22:50:07 +00:00
fix(e2e): stabilize tests and support Apple Silicon local dev (#4952)
- cross-compile musl binary on macOS via cargo-zigbuild, with arch-independent symlink so snapshots are stable across x86_64 and aarch64 - kill_running_sessions() now calls stop_zellij() instead of start_zellij(), eliminating 3s startup per test - prevent steps from firing prematurely by tightening completion conditions and adding re-send logic for garbled keys
This commit is contained in:
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
* fix: proper error on socket paths that are too long (https://github.com/zellij-org/zellij/pull/4957)
|
||||
* fix: scrollback with partial scroll region (eg. Codex bug) (https://github.com/zellij-org/zellij/pull/4941)
|
||||
* fix: remote attach issues on macOS (https://github.com/zellij-org/zellij/pull/4972)
|
||||
* test: improve e2e test reliability and local macOS dev support (https://github.com/zellij-org/zellij/pull/4952)
|
||||
|
||||
## [0.44.0] - 2026-03-23
|
||||
* fix: meta key handling in web client (https://github.com/zellij-org/zellij/pull/4376)
|
||||
|
||||
+8
-9
@@ -59,21 +59,20 @@ Zellij includes some end-to-end tests which test the whole application as a blac
|
||||
These tests work by running a docker container which contains the Zellij binary, connecting to it via ssh, sending some commands and comparing the output received against predefined snapshots.
|
||||
|
||||
<details>
|
||||
<summary>Should you be a macOS (including m1) user, please follow these commands before. (expand here):</summary>
|
||||
<summary>macOS prerequisites (expand here):</summary>
|
||||
|
||||
1. `rustup target add x86_64-unknown-linux-musl`
|
||||
2. `brew install messense/macos-cross-toolchains/x86_64-unknown-linux-musl`
|
||||
3. `export CC_x86_64_unknown_linux_musl=$(brew --prefix)/bin/x86_64-unknown-linux-musl-gcc`
|
||||
4. `export AR_x86_64_unknown_linux_musl=$(brew --prefix)/bin/x86_64-unknown-linux-musl-ar`
|
||||
5. `export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=$CC_x86_64_unknown_linux_musl`
|
||||
The build uses `cargo-zigbuild` to cross-compile a native musl binary (arm64 on Apple Silicon, amd64 on Intel) so the Docker container runs without emulation on any Mac.
|
||||
|
||||
1. `cargo install cargo-zigbuild`
|
||||
2. `brew install zig`
|
||||
</details>
|
||||
|
||||
|
||||
To run these tests locally, you'll need to have either `docker` or `podman` and also `docker-compose` installed.
|
||||
To run these tests locally, you'll need to have either `docker` or `podman` and also `docker compose` installed.
|
||||
Once you do, in the repository root:
|
||||
|
||||
1. `docker-compose up -d` will start up the docker container
|
||||
2. `cargo xtask ci e2e --build` will build the generic linux executable of Zellij in the target folder, which is shared with the container
|
||||
1. `docker compose up -d` will start up the docker container
|
||||
2. `cargo xtask ci e2e --build` will build the Zellij binary in the target folder, which is shared with the container
|
||||
3. `cargo xtask ci e2e --test` will run the tests
|
||||
|
||||
To re-run the tests after you've changed something in the code base, be sure to repeat steps 2 and 3.
|
||||
|
||||
+49
-31
@@ -296,6 +296,7 @@ pub fn scrolling_inside_a_pane() {
|
||||
let mut step_is_complete = false;
|
||||
if remote_terminal.cursor_position_is(63, 21)
|
||||
&& remote_terminal.snapshot_contains("line21")
|
||||
&& remote_terminal.status_bar_appears()
|
||||
{
|
||||
// all lines have been written to the pane
|
||||
remote_terminal.send_key(&SCROLL_MODE);
|
||||
@@ -314,8 +315,10 @@ pub fn scrolling_inside_a_pane() {
|
||||
if remote_terminal.cursor_position_is(63, 21)
|
||||
&& remote_terminal.snapshot_contains("line3 ")
|
||||
&& remote_terminal.snapshot_contains("SCROLL: 1/3")
|
||||
&& remote_terminal.snapshot_contains("PgDn|PgUp")
|
||||
{
|
||||
// keyboard scrolls up 1 line, scrollback is 4 lines: cat command + 2 extra lines from fixture + prompt
|
||||
// PgDn|PgUp only appears in the scroll mode status bar, confirming we're still in scroll mode
|
||||
step_is_complete = true;
|
||||
}
|
||||
step_is_complete
|
||||
@@ -1135,8 +1138,11 @@ pub fn detach_and_attach_session() {
|
||||
name: "Reattach session",
|
||||
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
|
||||
let mut step_is_complete = false;
|
||||
if !remote_terminal.status_bar_appears() {
|
||||
// we don't see the toolbar, so we can assume we've already detached
|
||||
if !remote_terminal.status_bar_appears()
|
||||
&& remote_terminal.snapshot_contains("Bye from Zellij!")
|
||||
{
|
||||
// we don't see the toolbar and Zellij's exit message is visible,
|
||||
// so Zellij has fully exited and the server is ready to accept connections
|
||||
remote_terminal.attach_to_original_session();
|
||||
step_is_complete = true;
|
||||
}
|
||||
@@ -1209,11 +1215,9 @@ pub fn quit_and_resurrect_session() {
|
||||
let last_snapshot = runner.take_snapshot_after(Step {
|
||||
name: "Wait for session to be resurrected",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
let mut step_is_complete = false;
|
||||
if remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)") {
|
||||
step_is_complete = true;
|
||||
}
|
||||
step_is_complete
|
||||
remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)")
|
||||
&& remote_terminal.status_bar_appears()
|
||||
&& remote_terminal.tab_bar_appears()
|
||||
},
|
||||
});
|
||||
if runner.test_timed_out && test_attempts > 0 {
|
||||
@@ -1270,11 +1274,9 @@ pub fn quit_and_resurrect_session_with_viewport_serialization() {
|
||||
let last_snapshot = runner.take_snapshot_after(Step {
|
||||
name: "Wait for session to be resurrected",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
let mut step_is_complete = false;
|
||||
if remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)") {
|
||||
step_is_complete = true;
|
||||
}
|
||||
step_is_complete
|
||||
remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)")
|
||||
&& remote_terminal.status_bar_appears()
|
||||
&& remote_terminal.tab_bar_appears()
|
||||
},
|
||||
});
|
||||
if runner.test_timed_out && test_attempts > 0 {
|
||||
@@ -2422,14 +2424,9 @@ pub fn send_blocking_command_through_the_cli() {
|
||||
if remote_terminal.status_bar_appears()
|
||||
&& remote_terminal.cursor_position_is(3, 2)
|
||||
{
|
||||
// Send a command that sleeps for 2 seconds then exits with status 42
|
||||
// Using bash -c with proper escaping
|
||||
// remote_terminal.send_key("aaa\n".as_bytes());
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
remote_terminal.send_blocking_command_through_the_cli(
|
||||
// "lskdjlskdjf"
|
||||
"bash -c 'sleep 2 && exit 42'",
|
||||
);
|
||||
remote_terminal
|
||||
.send_blocking_command_through_the_cli("bash -c 'sleep 2 && exit 42'");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
remote_terminal.send_key(&ENTER);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
@@ -2457,8 +2454,13 @@ pub fn send_blocking_command_through_the_cli() {
|
||||
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
|
||||
let mut step_is_complete = false;
|
||||
// After 2+ seconds, the command should complete and the floating pane should close
|
||||
// then we do echo $? to make sure we got back its exit status
|
||||
if !remote_terminal.snapshot_contains("PIN [ ]") {
|
||||
// Wait until the floating pane is gone AND the shell prompt is back before
|
||||
// asking for $?, otherwise we can race the blocking CLI process itself
|
||||
// returning to the shell.
|
||||
if !remote_terminal.snapshot_contains("PIN [ ]")
|
||||
&& remote_terminal.snapshot_contains("$ \u{2588}")
|
||||
&& remote_terminal.status_bar_appears()
|
||||
{
|
||||
remote_terminal.send_key("echo $?".as_bytes());
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
remote_terminal.send_key(&ENTER);
|
||||
@@ -2474,7 +2476,11 @@ pub fn send_blocking_command_through_the_cli() {
|
||||
name: "Verify CLI returned with proper exit status after command completed",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
let mut step_is_complete = false;
|
||||
// wait until echo $? is visible, the exit status rendered, and the cursor is back
|
||||
// at a blank prompt, which means the shell command actually executed
|
||||
if remote_terminal.snapshot_contains("echo $?")
|
||||
&& remote_terminal.snapshot_contains("│42")
|
||||
&& remote_terminal.snapshot_contains("$ \u{2588}")
|
||||
&& remote_terminal.status_bar_appears()
|
||||
{
|
||||
step_is_complete = true
|
||||
@@ -2664,6 +2670,7 @@ pub fn watcher_client_functionality() {
|
||||
// Step 1: Create main client and wait for it to load
|
||||
let mut main_client =
|
||||
RemoteRunner::new_with_session_name(fake_win_size, session_name, false)
|
||||
.retry_pause_ms(200)
|
||||
.dont_panic()
|
||||
.add_step(Step {
|
||||
name: "Wait for app to load",
|
||||
@@ -2674,12 +2681,16 @@ pub fn watcher_client_functionality() {
|
||||
});
|
||||
main_client.run_all_steps();
|
||||
|
||||
// Step 2: Start a background process that produces periodic output
|
||||
// Step 2: Start a foreground process that produces periodic output and then
|
||||
// replaces the shell with a long sleep. Using exec means pane 1 never returns
|
||||
// to an interactive shell prompt, so a SIGWINCH on re-attach cannot trigger a
|
||||
// prompt redraw — the snapshot is deterministic.
|
||||
main_client = main_client.add_step(Step {
|
||||
name: "Start background output loop",
|
||||
name: "Start foreground output loop",
|
||||
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
|
||||
remote_terminal.send_key(b"{ for i in 1 2 3; do sleep 1; echo \"WATCHER_OUTPUT_$i\"; done & } 2>/dev/null");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
remote_terminal.send_key(
|
||||
b"for i in 1 2 3; do sleep 1; echo WATCHER_OUTPUT_$i; done; echo WATCHER_DONE; exec sleep 1000000",
|
||||
);
|
||||
remote_terminal.send_key(&ENTER);
|
||||
true
|
||||
},
|
||||
@@ -2688,7 +2699,7 @@ pub fn watcher_client_functionality() {
|
||||
|
||||
// Step 3: Wait for first output line to appear
|
||||
main_client = main_client.add_step(Step {
|
||||
name: "Wait for first background output",
|
||||
name: "Wait for first output",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
let found = remote_terminal.snapshot_contains("WATCHER_OUTPUT_1");
|
||||
found
|
||||
@@ -2698,6 +2709,7 @@ pub fn watcher_client_functionality() {
|
||||
|
||||
// Step 4: Attach watcher client
|
||||
let mut watcher = RemoteRunner::new_watcher_session(fake_win_size, session_name)
|
||||
.retry_pause_ms(200)
|
||||
.dont_panic()
|
||||
.add_step(Step {
|
||||
name: "Wait for watcher to connect",
|
||||
@@ -2712,8 +2724,7 @@ pub fn watcher_client_functionality() {
|
||||
main_client = main_client.add_step(Step {
|
||||
name: "Split pane to the right",
|
||||
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
|
||||
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(1, 7)
|
||||
{
|
||||
if remote_terminal.status_bar_appears() {
|
||||
remote_terminal.send_key(&PANE_MODE);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
|
||||
@@ -2730,7 +2741,7 @@ pub fn watcher_client_functionality() {
|
||||
watcher = watcher.add_step(Step {
|
||||
name: "Watcher sees split pane",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
remote_terminal.cursor_position_is(63, 2) && remote_terminal.snapshot_contains("┐┌")
|
||||
remote_terminal.status_bar_appears() && remote_terminal.snapshot_contains("┐┌")
|
||||
// vertical split indicator
|
||||
},
|
||||
});
|
||||
@@ -2789,7 +2800,7 @@ pub fn watcher_client_functionality() {
|
||||
});
|
||||
main_client.run_all_steps();
|
||||
|
||||
// Step 11: Verify watcher receives background output even with no main client
|
||||
// Step 11: Verify watcher receives output even with no main client
|
||||
watcher = watcher.add_step(Step {
|
||||
name: "Watcher sees WATCHER_OUTPUT_2",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
@@ -2824,11 +2835,14 @@ pub fn watcher_client_functionality() {
|
||||
|
||||
watcher.run_all_steps();
|
||||
|
||||
// Step 15: Take final snapshots
|
||||
// Take final snapshots — WATCHER_DONE means the loop finished and pane 1 is now
|
||||
// running `sleep 1000000` (via exec), so no interactive shell and no prompt redraws.
|
||||
let main_client_snapshot = main_client_reattached.take_snapshot_after(Step {
|
||||
name: "Take main client final snapshot",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
remote_terminal.status_bar_appears()
|
||||
&& remote_terminal.snapshot_contains("WATCHER_DONE")
|
||||
&& remote_terminal.snapshot_contains("┐┌")
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2836,6 +2850,8 @@ pub fn watcher_client_functionality() {
|
||||
name: "Take watcher final snapshot",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
remote_terminal.status_bar_appears()
|
||||
&& remote_terminal.snapshot_contains("WATCHER_DONE")
|
||||
&& remote_terminal.snapshot_contains("┐┌")
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2885,7 +2901,9 @@ pub fn override_layout_from_default_to_compact() {
|
||||
let last_snapshot = runner.take_snapshot_after(Step {
|
||||
name: "Wait for compact layout to appear",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
// wait for both the layout indicator and the session name to render
|
||||
remote_terminal.snapshot_contains("NORMAL")
|
||||
&& remote_terminal.snapshot_contains("e2e-test")
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::path::Path;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij";
|
||||
const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/zellij";
|
||||
const SET_ENV_VARIABLES: &str = "EDITOR=/usr/bin/vi";
|
||||
const ZELLIJ_CONFIG_PATH: &str = "/usr/src/zellij/fixtures/configs";
|
||||
const ZELLIJ_CONFIG_DIRS_PATH: &str = "/usr/src/zellij/fixtures/config-dirs";
|
||||
@@ -28,6 +28,7 @@ const CONNECTION_USERNAME: &str = "test";
|
||||
const CONNECTION_PASSWORD: &str = "test";
|
||||
const SESSION_NAME: &str = "e2e-test";
|
||||
const RETRIES: usize = 10;
|
||||
const CLEANUP_DONE_MARKER: &str = "__ZELLIJ_CLEANUP_DONE__";
|
||||
|
||||
fn ssh_connect() -> ssh2::Session {
|
||||
let tcp = TcpStream::connect(CONNECTION_STRING).unwrap();
|
||||
@@ -75,6 +76,13 @@ fn stop_zellij(channel: &mut ssh2::Channel) {
|
||||
channel
|
||||
.write_all(b"rm -rf ~/.cache/zellij/permissions.kdl\n")
|
||||
.unwrap();
|
||||
// create an arch-independent symlink so the binary path in snapshots is stable across
|
||||
// x86_64 (CI) and aarch64 (Apple Silicon local dev)
|
||||
channel
|
||||
.write_all(
|
||||
b"ln -sf /usr/src/zellij/$(uname -m)-unknown-linux-musl/release/zellij /usr/src/zellij/zellij\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn start_zellij(channel: &mut ssh2::Channel) {
|
||||
@@ -89,7 +97,6 @@ fn start_zellij(channel: &mut ssh2::Channel) {
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn start_zellij_with_config_dir(channel: &mut ssh2::Channel, config_dir: &str) {
|
||||
@@ -104,7 +111,6 @@ fn start_zellij_with_config_dir(channel: &mut ssh2::Channel, config_dir: &str) {
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) {
|
||||
@@ -119,7 +125,6 @@ fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) {
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn start_zellij_mirrored_session_with_layout(channel: &mut ssh2::Channel, layout_file_name: &str) {
|
||||
@@ -138,7 +143,6 @@ fn start_zellij_mirrored_session_with_layout(channel: &mut ssh2::Channel, layout
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn start_zellij_mirrored_session_with_layout_and_viewport_serialization(
|
||||
@@ -160,7 +164,6 @@ fn start_zellij_mirrored_session_with_layout_and_viewport_serialization(
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirrored: bool) {
|
||||
@@ -179,7 +182,6 @@ fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirr
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
|
||||
@@ -193,7 +195,6 @@ fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn watch_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
|
||||
@@ -207,7 +208,6 @@ fn watch_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn start_zellij_without_frames(channel: &mut ssh2::Channel) {
|
||||
@@ -222,7 +222,6 @@ fn start_zellij_without_frames(channel: &mut ssh2::Channel) {
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn start_zellij_with_config(channel: &mut ssh2::Channel, config_path: &str) {
|
||||
@@ -241,7 +240,40 @@ fn start_zellij_with_config(channel: &mut ssh2::Channel, config_path: &str) {
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
|
||||
fn wait_for_startup(last_snapshot: &Arc<Mutex<String>>) {
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(10);
|
||||
loop {
|
||||
if last_snapshot.lock().unwrap().contains("Ctrl +") {
|
||||
break;
|
||||
}
|
||||
if start.elapsed() > timeout {
|
||||
break; // timed out — let the test proceed and fail naturally
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_for_shell_output(channel: &mut ssh2::Channel, needle: &str) {
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(10);
|
||||
let mut output = String::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
if output.contains(needle) || start.elapsed() > timeout {
|
||||
break;
|
||||
}
|
||||
match channel.read(&mut buf) {
|
||||
Ok(0) => std::thread::sleep(std::time::Duration::from_millis(50)),
|
||||
Ok(count) => output.push_str(&String::from_utf8_lossy(&buf[..count])),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
},
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_channel(
|
||||
@@ -431,13 +463,15 @@ impl RemoteTerminal {
|
||||
.unwrap();
|
||||
}
|
||||
pub fn attach_to_original_session(&mut self) {
|
||||
let mut channel = self.channel.lock().unwrap();
|
||||
channel
|
||||
.write_all(
|
||||
format!("{} attach {}\n", ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME).as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
{
|
||||
let mut channel = self.channel.lock().unwrap();
|
||||
channel
|
||||
.write_all(
|
||||
format!("{} attach {}\n", ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME).as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
} // release mutex before sleeping so the reader thread can process Zellij's startup output
|
||||
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
|
||||
}
|
||||
pub fn run_zellij_action(&mut self, action_and_arguments: &str) {
|
||||
@@ -533,6 +567,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -571,6 +606,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -609,6 +645,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -647,6 +684,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -691,6 +729,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -709,7 +748,13 @@ impl RemoteRunner {
|
||||
let sess = ssh_connect();
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
setup_remote_environment(&mut channel, win_size);
|
||||
start_zellij(&mut channel);
|
||||
stop_zellij(&mut channel);
|
||||
channel
|
||||
.write_all(format!("printf '{}\\n'\n", CLEANUP_DONE_MARKER).as_bytes())
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
sess.set_blocking(false);
|
||||
wait_for_shell_output(&mut channel, CLEANUP_DONE_MARKER);
|
||||
}
|
||||
pub fn new_with_session_name(win_size: Size, session_name: &str, mirrored: bool) -> Self {
|
||||
// notice that this method does not have a timeout, so use with caution!
|
||||
@@ -736,6 +781,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -774,6 +820,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -812,6 +859,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -850,6 +898,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
@@ -889,6 +938,7 @@ impl RemoteRunner {
|
||||
sess.set_blocking(false);
|
||||
let reader_thread =
|
||||
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
|
||||
wait_for_startup(&last_snapshot);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ source: src/tests/e2e/cases.rs
|
||||
expression: last_snapshot
|
||||
---
|
||||
┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij action override-layout compact │
|
||||
│$ /usr/src/zellij/zellij action override-layout compact │
|
||||
│$ │
|
||||
│$ █ │
|
||||
│ │
|
||||
@@ -25,4 +25,4 @@ expression: last_snapshot
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
Zellij (e2e-test) NORMAL Tab #1
|
||||
Zellij (e2e-test) NORMAL Tab #1
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ expression: last_snapshot
|
||||
---
|
||||
Zellij (e2e-test) Tab #1
|
||||
┌ Pane #1 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij run --blocking --floating --close-on-exit -- bash -c 'sleep 2 && exit 42' │
|
||||
│$ /usr/src/zellij/zellij run --blocking --floating --close-on-exit -- bash -c 'sleep 2 && exit 42' │
|
||||
│$ │
|
||||
│$ echo $? │
|
||||
│42 │
|
||||
@@ -25,4 +25,4 @@ expression: last_snapshot
|
||||
│ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT Alt + <n> New Pane <f> Floating
|
||||
Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT Alt + <n> New Pane <f> Floating
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ expression: last_snapshot
|
||||
---
|
||||
Zellij (e2e-test) Tab #1 Alt <[]> VERTICAL
|
||||
┌ Pane #1 ────────────────────────────────────────────────────────────────┐┌ /usr/src/zellij/fixtures/append-echo-script.sh ─────────────────────────┐
|
||||
│$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij run -s -- "/us││foo │
|
||||
│r/src/zellij/fixtures/append-echo-script.sh" ││foo │
|
||||
│$ /usr/src/zellij/zellij run -s -- "/usr/src/zellij/fixtures/append-echo-││foo │
|
||||
│script.sh" ││foo │
|
||||
│terminal_1 ││█ │
|
||||
│$ ││ │
|
||||
│$ ││ │
|
||||
|
||||
+5
-5
@@ -4,13 +4,13 @@ expression: watcher_snapshot
|
||||
---
|
||||
Zellij (watcher_client_functionality) Tab #1
|
||||
┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐
|
||||
│$ { for i in 1 2 3; do sleep 1; echo "WATCHER_OUTPUT_$i"; ││$ █ │
|
||||
│done & } 2>/dev/null ││ │
|
||||
│$ ││ │
|
||||
│$ WATCHER_OUTPUT_1 ││ │
|
||||
│$ for i in 1 2 3; do sleep 1; echo WATCHER_OUTPUT_$i; done││$ █ │
|
||||
│; echo WATCHER_DONE; exec sleep 1000000 ││ │
|
||||
│WATCHER_OUTPUT_1 ││ │
|
||||
│WATCHER_OUTPUT_2 ││ │
|
||||
│WATCHER_OUTPUT_3 ││ │
|
||||
│$ ││ │
|
||||
│WATCHER_DONE ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
|
||||
+5
-5
@@ -4,13 +4,13 @@ expression: main_client_snapshot
|
||||
---
|
||||
Zellij (watcher_client_functionality) Tab #1
|
||||
┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐
|
||||
│$ { for i in 1 2 3; do sleep 1; echo "WATCHER_OUTPUT_$i"; ││$ █ │
|
||||
│done & } 2>/dev/null ││ │
|
||||
│$ ││ │
|
||||
│$ WATCHER_OUTPUT_1 ││ │
|
||||
│$ for i in 1 2 3; do sleep 1; echo WATCHER_OUTPUT_$i; done││$ █ │
|
||||
│; echo WATCHER_DONE; exec sleep 1000000 ││ │
|
||||
│WATCHER_OUTPUT_1 ││ │
|
||||
│WATCHER_OUTPUT_2 ││ │
|
||||
│WATCHER_OUTPUT_3 ││ │
|
||||
│$ ││ │
|
||||
│WATCHER_DONE ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
|
||||
@@ -58,12 +58,18 @@ pub fn type_second_tab_content() -> Step {
|
||||
Step {
|
||||
name: "Type second tab content",
|
||||
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
|
||||
let mut step_is_complete = false;
|
||||
if remote_terminal.snapshot_contains("Tab #2 content") {
|
||||
return true;
|
||||
}
|
||||
// send (or re-send if the previous attempt arrived garbled) and wait for the next
|
||||
// retry to confirm the content appeared correctly in the terminal
|
||||
if remote_terminal.status_bar_appears() {
|
||||
remote_terminal.send_key(&SECOND_TAB_CONTENT);
|
||||
step_is_complete = true;
|
||||
// sleep longer than the reader's idle poll (100ms) so the snapshot is updated
|
||||
// before the next retry fires, preventing a spurious re-send
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
}
|
||||
step_is_complete
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+40
-8
@@ -89,23 +89,55 @@ fn e2e_build(sh: &Shell) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
let _pd = sh.push_dir(project_root);
|
||||
crate::cargo()
|
||||
.and_then(|cargo| {
|
||||
// On Windows, build natively (MSVC). On Unix, cross-compile for musl.
|
||||
if cfg!(windows) {
|
||||
// On Windows, build natively (MSVC).
|
||||
// On macOS: use cargo-zigbuild which handles musl cross-compilation for any arch without
|
||||
// needing a separate linker toolchain. The target arch matches the host so the Docker
|
||||
// container runs natively without emulation (arm64 on Apple Silicon, amd64 on Intel).
|
||||
// Install with: cargo install cargo-zigbuild && brew install zig
|
||||
// On Linux: cross-compile for musl.
|
||||
if cfg!(windows) {
|
||||
crate::cargo()
|
||||
.and_then(|cargo| {
|
||||
cmd!(sh, "{cargo} build --release")
|
||||
.run()
|
||||
.map_err(anyhow::Error::new)
|
||||
} else {
|
||||
})
|
||||
.context(err_context)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
let target = if cfg!(target_arch = "aarch64") {
|
||||
"aarch64-unknown-linux-musl"
|
||||
} else {
|
||||
"x86_64-unknown-linux-musl"
|
||||
};
|
||||
crate::cargo()
|
||||
.and_then(|cargo| {
|
||||
if which::which("cargo-zigbuild").is_err() {
|
||||
eprintln!("!! 'cargo-zigbuild' wasn't found but is needed on macOS.");
|
||||
eprintln!("!! Please install it with:");
|
||||
eprintln!("!! cargo install cargo-zigbuild");
|
||||
eprintln!("!! brew install zig");
|
||||
return Err(anyhow::anyhow!("couldn't find 'cargo-zigbuild'"));
|
||||
}
|
||||
cmd!(sh, "rustup target add {target}")
|
||||
.run()
|
||||
.map_err(anyhow::Error::new)?;
|
||||
cmd!(sh, "{cargo} zigbuild --release --target {target}")
|
||||
.run()
|
||||
.map_err(anyhow::Error::new)
|
||||
})
|
||||
.context(err_context)
|
||||
} else {
|
||||
crate::cargo()
|
||||
.and_then(|cargo| {
|
||||
cmd!(
|
||||
sh,
|
||||
"{cargo} build --release --target x86_64-unknown-linux-musl"
|
||||
)
|
||||
.run()
|
||||
.map_err(anyhow::Error::new)
|
||||
}
|
||||
})
|
||||
.context(err_context)
|
||||
})
|
||||
.context(err_context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Native release build: builds plugins, generates protobufs, and runs
|
||||
|
||||
Reference in New Issue
Block a user