diff options
23 files changed, 1576 insertions, 10 deletions
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..a81d4ae61 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,49 @@ +name: End to End tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Bulild generic binary and run tests on it + runs-on: ubuntu-latest + + services: + ssh: + image: ghcr.io/linuxserver/openssh-server + env: + PUID: 1000 + PGID: 1000 + TZ: Europe/Vienna + PASSWORD_ACCESS: true + USER_PASSWORD: test + USER_NAME: test + ports: + - 2222:2222 + options: -v ${{ github.workspace }}/target:/usr/src/zellij --name ssh + steps: + - uses: actions/checkout@v2 + - name: Add WASM target + run: rustup target add wasm32-wasi + - name: Install musl-tools + run: sudo apt-get install -y --no-install-recommends musl-tools + - name: Add musl target + run: rustup target add x86_64-unknown-linux-musl + - name: Install cargo-make + run: cargo install --debug cargo-make + - name: Build asset + run: cargo make build-e2e + - name: Restart ssh container + # we need to do this because otherwise the volume will not be mounted + # on the docker container, since it was created before the folder existed + uses: docker://docker + with: + args: docker restart ssh + - name: Test + run: cargo make e2e-test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95ac05ec4..d34367f33 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,18 @@ cargo make manpage To run `install` or `publish`, you'll need the package `binaryen` in the version `wasm-opt --version` > 97, for it's command `wasm-opt`. +## Running the end-to-end tests +Zellij includes some end to end tests which test the whole application as a black-box from the outside. +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. + +To run these tests locally, you'll need to have both `docker` and `docker-compose` installed. +Once you do, in the repository root: +1. `docker-compose up -d` will start up the docker container +2. `cargo make build-e2e` will build the generic linux executable of Zellij in the target folder, which is shared with the container +3. `cargo make 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. + ## Looking for something to work on? If you are new contributor to `Zellij` going through diff --git a/Cargo.lock b/Cargo.lock index 2996a8182..a2c525d65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,6 +313,15 @@ dependencies = [ ] [[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] name = "colored" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -504,7 +513,7 @@ dependencies = [ "lazy_static", "libc", "mio", - "parking_lot", + "parking_lot 0.11.1", "signal-hook 0.1.17", "winapi", ] @@ -902,6 +911,7 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1b21a2971cea49ca4613c0e9fe8225ecaf5de64090fddc6002284726e9244" dependencies = [ + "backtrace", "console", "lazy_static", "serde", @@ -1018,6 +1028,32 @@ dependencies = [ ] [[package]] +name = "libssh2-sys" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1025,6 +1061,15 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lock_api" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" @@ -1185,6 +1230,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] +name = "openssl-sys" +version = "0.9.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "parking" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1192,13 +1250,37 @@ checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" [[package]] name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + +[[package]] +name = "parking_lot" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", - "lock_api", - "parking_lot_core", + "lock_api 0.4.4", + "parking_lot_core 0.8.3", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "smallvec", + "winapi", ] [[package]] @@ -1210,7 +1292,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.8", "smallvec", "winapi", ] @@ -1228,6 +1310,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] name = "polling" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1424,6 +1512,12 @@ dependencies = [ [[package]] name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" @@ -1437,7 +1531,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" dependencies = [ - "redox_syscall", + "redox_syscall 0.2.8", ] [[package]] @@ -1447,7 +1541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.8", ] [[package]] @@ -1622,7 +1716,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" dependencies = [ - "lock_api", + "lock_api 0.4.4", +] + +[[package]] +name = "ssh2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d876d4d57f6bbf2245d43f7ec53759461f801a446d3693704aa6d27b257844d7" +dependencies = [ + "bitflags", + "libc", + "libssh2-sys", + "parking_lot 0.10.2", ] [[package]] @@ -1749,7 +1855,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "rand 0.8.3", - "redox_syscall", + "redox_syscall 0.2.8", "remove_dir_all", "winapi", ] @@ -1783,7 +1889,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" dependencies = [ "libc", "numtoa", - "redox_syscall", + "redox_syscall 0.2.8", "redox_termios", ] @@ -1930,6 +2036,12 @@ dependencies = [ ] [[package]] +name = "vcpkg" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025ce40a007e1907e58d5bc1a594def78e5573bb0b1160bc389634e8f12e4faa" + +[[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2333,6 +2445,8 @@ version = "0.14.0" dependencies = [ "insta", "names", + "rand 0.8.3", + "ssh2", "zellij-client", "zellij-server", "zellij-utils", diff --git a/Cargo.toml b/Cargo.toml index 7e483940b..165254525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,9 @@ zellij-server = { path = "zellij-server/", version = "0.14.0" } zellij-utils = { path = "zellij-utils/", version = "0.14.0" } [dev-dependencies] -insta = "1.6.0" +insta = { version = "1.6.0", features = ["backtrace"] } +ssh2 = "0.9.1" +rand = "0.8.0" zellij-utils = { path = "zellij-utils/", version = "0.14.0", features = ["test"] } zellij-client = { path = "zellij-client/", version = "0.14.0", features = ["test"] } zellij-server = { path = "zellij-server/", version = "0.14.0", features = ["test"] } diff --git a/Makefile.toml b/Makefile.toml index 16b7695ba..e0043ab71 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -84,6 +84,10 @@ end env = { "CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS" = ["default-plugins/status-bar", "default-plugins/strider", "default-plugins/tab-bar"] } run_task = { name = "build-release", fork = true } +[tasks.build-plugins] +env = { "CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS" = ["default-plugins/status-bar", "default-plugins/strider", "default-plugins/tab-bar"] } +run_task = { name = "build", fork = true } + [tasks.wasm-opt-plugins] script_runner = "@duckscript" script = ''' @@ -120,6 +124,19 @@ dependencies = ["setup-cross-compilation", "build-plugins-release", "wasm-opt-pl command = "cross" args = ["build", "--verbose", "--release", "--target", "${CARGO_MAKE_TASK_ARGS}"] +# Build e2e asset +[tasks.build-e2e] +workspace = false +dependencies = ["build-plugins"] +command = "cargo" +args = ["build", "--verbose", "--target", "x86_64-unknown-linux-musl"] + +# Run e2e tests - we mark the e2e tests as "ignored" so they will not be run with the normal ones +[tasks.e2e-test] +workspace = false +command = "cargo" +args = ["test", "--", "--ignored", "--nocapture", "--test-threads", "1", "@@split(CARGO_MAKE_TASK_ARGS,;)"] + [tasks.setup-cross-compilation] command = "cargo" args = ["install", "cross"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ec52486ee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +--- +version: "2.1" +services: + zellij-e2e: + image: ghcr.io/linuxserver/openssh-server + container_name: zellij-e2e + hostname: zellij-e2e + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/Vienna + - PASSWORD_ACCESS=true + - USER_PASSWORD=test + - USER_NAME=test + volumes: + - type: bind + source: ./target + target: /usr/src/zellij + ports: + - 2222:2222 + restart: unless-stopped diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs new file mode 100644 index 000000000..5af67074c --- /dev/null +++ b/src/tests/e2e/cases.rs @@ -0,0 +1,716 @@ +use ::insta::assert_snapshot; +use zellij_utils::pane_size::PositionAndSize; + +use rand::Rng; + +use super::remote_runner::{RemoteRunner, RemoteTerminal, Step}; +use crate::tests::utils::commands::{ + CLOSE_PANE_IN_PANE_MODE, DETACH_IN_SESSION_MODE, ENTER, LOCK_MODE, NEW_TAB_IN_TAB_MODE, + PANE_MODE, QUIT, RESIZE_LEFT_IN_RESIZE_MODE, RESIZE_MODE, SCROLL_MODE, + SCROLL_UP_IN_SCROLL_MODE, SESSION_MODE, SPLIT_RIGHT_IN_PANE_MODE, TAB_MODE, + TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE, +}; + +// All the E2E tests are marked as "ignored" so that they can be run separately from the normal +// tests + +#[test] +#[ignore] +pub fn starts_with_one_terminal() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("starts_with_one_terminal", fake_win_size, None) + .add_step(Step { + name: "Wait for app to load", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn split_terminals_vertically() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + + let last_snapshot = RemoteRunner::new("split_terminals_vertically", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for new pane to appear", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { + let fake_win_size = PositionAndSize { + cols: 8, + rows: 20, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new( + "cannot_split_terminals_vertically_when_active_terminal_is_too_small", + fake_win_size, + None, + ) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Send text to terminal", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + // this is just normal input that should be sent into the one terminal so that we can make + // sure we silently failed to split in the previous step + remote_terminal.send_key(&"Hi!".as_bytes()); + true + }, + }) + .add_step(Step { + name: "Wait for text to appear", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(5, 2) && remote_terminal.snapshot_contains("Hi!") + { + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn scrolling_inside_a_pane() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("scrolling_inside_a_pane", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Fill terminal with text", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + remote_terminal.send_key(&format!("{:0<57}", "line1 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line2 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line3 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line4 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line5 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line6 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line7 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line8 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line9 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line10 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line11 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line12 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line13 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line14 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line15 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line16 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line17 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line18 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line19 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<58}", "line20 ").as_bytes()); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Scroll up inside pane", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(119, 20) { + // all lines have been written to the pane + remote_terminal.send_key(&SCROLL_MODE); + remote_terminal.send_key(&SCROLL_UP_IN_SCROLL_MODE); |