From 3b5771000622ee02bba414cadd4419d466fd8116 Mon Sep 17 00:00:00 2001 From: alexandre pasmantier Date: Mon, 16 Jun 2025 01:43:18 +0200 Subject: [PATCH] test(e2e): more end to end tests --- Cargo.lock | 113 +++++++++++------ Cargo.toml | 2 +- output.txt | Bin 0 -> 5052 bytes tests/e2e.rs | 341 +++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 329 insertions(+), 127 deletions(-) create mode 100644 output.txt diff --git a/Cargo.lock b/Cargo.lock index eaa955d..f21e653 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" @@ -231,6 +231,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.41" @@ -347,12 +353,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "comma" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" - [[package]] name = "compact_str" version = "0.8.1" @@ -585,6 +585,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "either" version = "1.15.0" @@ -1031,15 +1037,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1069,16 +1066,14 @@ dependencies = [ [[package]] name = "nix" -version = "0.25.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "autocfg", - "bitflags 1.2.1", + "bitflags 2.9.0", "cfg-if", + "cfg_aliases", "libc", - "memoffset", - "pin-utils", ] [[package]] @@ -1295,6 +1290,27 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -1431,19 +1447,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "rexpect" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ff60778f96fb5a48adbe421d21bf6578ed58c0872d712e7e08593c195adff8" -dependencies = [ - "comma", - "nix", - "regex", - "tempfile", - "thiserror 1.0.69", -] - [[package]] name = "ring" version = "0.17.14" @@ -1614,6 +1617,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serial2" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d1d08630509d69f90eff4afcd02c3bd974d979225cbd815ff5942351b14375" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1634,6 +1648,22 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -1789,8 +1819,8 @@ dependencies = [ "lazy-regex", "nucleo", "parking_lot", + "portable-pty", "ratatui", - "rexpect", "rustc-hash", "serde", "serde_json", @@ -2421,6 +2451,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index ed9316d..cb4b2e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ clipboard-win = "5.4.0" [dev-dependencies] criterion = { version = "0.5", features = ["async_tokio"] } tempfile = "3.16.0" -rexpect = "0.5" +portable-pty = "0.9.0" [build-dependencies] diff --git a/output.txt b/output.txt new file mode 100644 index 0000000000000000000000000000000000000000..4ceccb4a3d4d4786f1d44694ab36edae57eed13f GIT binary patch literal 5052 zcmeHJ%}(1u5bk^AvK1Go6tLGnv1(?1(*ei$66(JgInU_@o-O z;f(I-L8WClp5-~7>Bi&Hukpj(jDK(>76&LEAm4gXI3!@5X6szhOf6_&^a#d;D&1hZ zQ29|9h36@0_et1K2R9^5$mJmX(F?Dc4X~to`v{8=!ua7{am+mjOUms`ZmV)z%4?atrpjwl?qqUDl{-@IW^z}RyHc)exMB1hU4A5+emaP% zoh_09KrD?RVSlX@02&vt0064AEc~-gE$l7N1ema{QMw19R|^1Ph&1?*UBs|QbU>GG z=pe8>n&FEz4#-)vaeU&d4!Zf0AM7;t>Icoe_4oq)$kq!OuC#3COWtp&z>WRpZvC|R z`P6SVc2E3crlJNr;rsY-|I^9weuL|U_6+>?pp&epNesb&nc!gGuQ!hB3kAPGp8ok( z-7~NZ3Uidvlh~k;QL!SB@*@3AQQ{dk8e>q<;l(qY%(#iATER6_3;b@o-wC6ngB_}z zp$HrJHrUe%b|NT~<(V|-Cu#dE3h~R*f^&9$h=_pQCVNUt3>2I1t5wqL^pinIqFz5G zgDX%4y_-8|vG0;&aeMInBDwD4x#~r)(sI;eHUbCK)G*Mw8)kv6x`#fN4I5bXbOVHW z3jn5Jc1@H+3>e*`a>l()CVUlS{wmF!wTInmw>M0a!3|b#f@Tn$+@*<$p3NzSYjg?ujkT0SH5aL? z9OJ07#N9Vx@zE2uj}?^9L@@#LJW`R&yyx<2Lk5fYpTjUt(vbKGTt$*dEiqhQunU3z zA+|!;0*idI=5`Gt+8FAGZH%W&QnJ8B2C@(18bQDsAx97&2;;k-bGe3EsLAPrN=Oyx zLoke}s|@hX$_KX!X?Q;8Yv^(o9;2`_*MY)vwo}V2BH_1A*eS#>byVxX zgih9!@FD}r()7~Ssrq9^kxZx9a#dTvw(R1B1iNgP7sOi#Tt|>W4LEe|2Sz+;ZEc698r#-ohuZS8u!b-SW50z%m2N3@kIS%)tMV Ff#2w^jRpV! literal 0 HcmV?d00001 diff --git a/tests/e2e.rs b/tests/e2e.rs index b6f4158..6bc2a86 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,97 +1,260 @@ -use std::path::Path; +use std::thread::sleep; +use std::time::Duration; -use rexpect::error::Error; -use rexpect::process::wait::WaitStatus; -use rexpect::spawn; -use television::channels::prototypes::ChannelPrototype; -use tempfile::TempDir; +use portable_pty::{CommandBuilder, PtySize, native_pty_system}; -#[allow(dead_code)] -fn setup_config(content: &str) -> TempDir { - let temp_dir = tempfile::tempdir().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - std::fs::write(&config_path, content).unwrap(); - temp_dir +const DEFAULT_CONFIG_FILE: &str = "./.config/config.toml"; +#[cfg(unix)] +const DEFAULT_CABLE_DIR: &str = "./cable/unix"; +#[cfg(windows)] +const DEFAULT_CABLE_DIR: &str = "./cable/windows"; + +const DEFAULT_PTY_SIZE: PtySize = PtySize { + rows: 20, + cols: 60, + pixel_width: 0, + pixel_height: 0, +}; + +const DEFAULT_TIMEOUT: Duration = Duration::from_millis(400); + +fn assert_exit_ok( + child: &mut Box, + timeout: Duration, +) { + let now = std::time::Instant::now(); + while now.elapsed() < timeout { + match child.try_wait() { + Ok(Some(status)) => { + assert!( + status.success(), + "Process exited with non-zero status: {:?}", + status + ); + return; + } + Ok(None) => { + // Process is still running, continue waiting + sleep(Duration::from_millis(50)); + } + Err(e) => { + panic!("Error waiting for process: {}", e); + } + } + } + panic!("Process did not exit in time"); } -fn setup_cable_channels(channels: Vec<&str>, cable_dir: D) -where - D: AsRef, -{ - let cable_dir = cable_dir.as_ref(); - std::fs::create_dir_all(cable_dir).unwrap(); - for channel in channels { - let name = toml::from_str::(channel) - .unwrap() - .metadata - .name; - let channel_path = cable_dir.join(format!("{}.toml", name)); - std::fs::write(&channel_path, channel).unwrap(); +#[test] +fn tv_version() { + let pty_system = native_pty_system(); + // Create a new pty + let pair = pty_system.openpty(DEFAULT_PTY_SIZE).unwrap(); + + // Spawn a tv process in the pty + let mut cmd = CommandBuilder::new("./target/debug/tv"); + cmd.cwd(std::env::current_dir().unwrap()); + cmd.args(["--version"]); + let mut child: Box = + pair.slave.spawn_command(cmd).unwrap(); + sleep(Duration::from_millis(200)); + + // Read the output from the pty + let mut buf = [0; 512]; + let mut reader = pair.master.try_clone_reader().unwrap(); + let _ = reader.read(&mut buf).unwrap(); + let output = String::from_utf8_lossy(&buf); + assert!(output.contains("television")); + + assert_exit_ok(&mut child, DEFAULT_TIMEOUT); +} + +#[test] +fn tv_help() { + let pty_system = native_pty_system(); + // Create a new pty + let pair = pty_system.openpty(DEFAULT_PTY_SIZE).unwrap(); + + // Spawn a tv process in the pty + let mut cmd = CommandBuilder::new("./target/debug/tv"); + cmd.cwd(std::env::current_dir().unwrap()); + cmd.args(["--help"]); + let mut child = pair.slave.spawn_command(cmd).unwrap(); + sleep(Duration::from_millis(200)); + + // Read the output from the pty + let mut buf = [0; 512]; + let mut reader = pair.master.try_clone_reader().unwrap(); + let _ = reader.read(&mut buf).unwrap(); + let output = String::from_utf8_lossy(&buf); + assert!(output.contains("A cross-platform")); + + assert_exit_ok(&mut child, DEFAULT_TIMEOUT); +} + +#[test] +fn tv_list_channels() { + let pty_system = native_pty_system(); + // Create a new pty + let pair = pty_system.openpty(DEFAULT_PTY_SIZE).unwrap(); + + // Spawn a tv process in the pty + let mut cmd = CommandBuilder::new("./target/debug/tv"); + cmd.cwd(std::env::current_dir().unwrap()); + cmd.args([ + "--cable-dir", + DEFAULT_CABLE_DIR, + "--config-file", + DEFAULT_CONFIG_FILE, + "list-channels", + ]); + let mut child = pair.slave.spawn_command(cmd).unwrap(); + sleep(Duration::from_millis(200)); + + // Read the output from the pty + let mut buf = [0; 512]; + let mut reader = pair.master.try_clone_reader().unwrap(); + let _ = reader.read(&mut buf).unwrap(); + let output = String::from_utf8_lossy(&buf); + assert!(output.contains("files"), "Output: {}", output); + assert!(output.contains("dirs"), "Output: {}", output); + + assert_exit_ok(&mut child, DEFAULT_TIMEOUT); +} + +#[test] +fn tv_init_zsh() { + let pty_system = native_pty_system(); + // Create a new pty + let pair = pty_system.openpty(DEFAULT_PTY_SIZE).unwrap(); + + // Spawn a tv process in the pty + let mut cmd = CommandBuilder::new("./target/debug/tv"); + cmd.cwd(std::env::current_dir().unwrap()); + cmd.args([ + "--cable-dir", + DEFAULT_CABLE_DIR, + "--config-file", + DEFAULT_CONFIG_FILE, + "init", + "zsh", + ]); + let mut child = pair.slave.spawn_command(cmd).unwrap(); + sleep(Duration::from_millis(200)); + + assert_exit_ok(&mut child, DEFAULT_TIMEOUT); +} + +/// Creates a command to run `tv` with the repo's config and cable directory +/// using the cable directory as cwd. +fn tv_command(channel: &str) -> CommandBuilder { + let mut cmd = CommandBuilder::new("./target/debug/tv"); + cmd.cwd(std::env::current_dir().unwrap()); + cmd.args([ + "--cable-dir", + DEFAULT_CABLE_DIR, + "--config-file", + DEFAULT_CONFIG_FILE, + channel, + ]); + cmd +} + +#[test] +fn tv_ctrl_c() { + let pty_system = native_pty_system(); + // Create a new pty + let pair = pty_system.openpty(DEFAULT_PTY_SIZE).unwrap(); + + // Spawn a tv process in the pty + let mut child = pair.slave.spawn_command(tv_command("files")).unwrap(); + sleep(Duration::from_millis(200)); + + // Send Ctrl-C to the process + let mut writer = pair.master.take_writer().unwrap(); + writeln!(writer, "\x03").unwrap(); // Ctrl-C + + // Check if the child process exited with a timeout + assert_exit_ok(&mut child, DEFAULT_TIMEOUT); +} + +#[test] +fn tv_remote_control() { + let pty_system = native_pty_system(); + // Create a new pty + let pair = pty_system.openpty(DEFAULT_PTY_SIZE).unwrap(); + + // Spawn a tv process in the pty + let mut child = pair.slave.spawn_command(tv_command("files")).unwrap(); + sleep(Duration::from_millis(200)); + + // Send Ctrl-T to the process (open remote control mode) + let mut writer = pair.master.take_writer().unwrap(); + writeln!(writer, "\x14").unwrap(); // Ctrl-T + sleep(Duration::from_millis(200)); + + let mut buf = [0; 5096]; // Buffer size for reading output + let mut reader = pair.master.try_clone_reader().unwrap(); + let _ = reader.read(&mut buf).unwrap(); + + let s = String::from_utf8_lossy(&buf); + + assert!(s.contains("Remote Control")); + + // Send Ctrl-c to exit remote control mode + writeln!(writer, "\x03").unwrap(); + sleep(Duration::from_millis(200)); + // resend Ctrl-c to finally exit + writeln!(writer, "\x03").unwrap(); + sleep(Duration::from_millis(200)); + + assert_exit_ok(&mut child, DEFAULT_TIMEOUT); +} + +macro_rules! test_channel { + ($($name:ident: $channel_name:expr,)*) => { + $( + #[test] + fn $name() { + let pty_system = native_pty_system(); + // Create a new pty + let pair = pty_system.openpty(DEFAULT_PTY_SIZE).unwrap(); + let cmd = tv_command(&$channel_name); + + let mut child = pair.slave.spawn_command(cmd).unwrap(); + sleep(Duration::from_millis(300)); + + // Read the output from the pty + let mut buf = [0; 5096]; + let mut reader = pair.master.try_clone_reader().unwrap(); + let _ = reader.read(&mut buf).unwrap(); + let output = String::from_utf8_lossy(&buf); + + assert!( + output.contains(&$channel_name), + "Unable to find channel name (\"{}\") in output: {}", + &$channel_name, + output + ); + + // Send Ctrl-C to the process + let mut writer = pair.master.take_writer().unwrap(); + writeln!(writer, "\x03").unwrap(); // Ctrl-C + sleep(Duration::from_millis(200)); + + assert_exit_ok(&mut child, DEFAULT_TIMEOUT); + } + )* } } -#[test] -fn tv_version() -> Result<(), Error> { - let mut p = spawn("./target/debug/tv --version", Some(500))?; - p.exp_regex("television [0-9]+\\.[0-9]+\\.[0-9]+")?; - - Ok(()) -} - -#[test] -fn tv_help() -> Result<(), Error> { - let mut p = spawn("./target/debug/tv --help", Some(500))?; - p.exp_regex("A cross-platform")?; - - Ok(()) -} - -const BASIC_FILE_CHANNEL: &str = r#" - [metadata] - name = "files" - - [source] - command = "fd -t f" -"#; - -const BASIC_DIR_CHANNEL: &str = r#" - [metadata] - name = "dirs" - - [source] - command = "fd -t d" -"#; - -#[test] -fn tv_list_channels() -> Result<(), Error> { - let temp_dir = TempDir::new().unwrap(); - setup_cable_channels( - vec![BASIC_FILE_CHANNEL, BASIC_DIR_CHANNEL], - &temp_dir, - ); - - let mut p = spawn( - &format!( - "./target/debug/tv --cable-dir {} list-channels ", - temp_dir.path().display() - ), - Some(500), - )?; - p.exp_regex("files")?; - p.exp_regex("dirs")?; - - Ok(()) -} - -#[test] -fn tv_init_zsh() -> Result<(), Error> { - let p = spawn("./target/debug/tv init zsh", Some(500))?; - // check that the process exits successfully - if let Ok(w) = p.process.wait() { - assert_eq!(w, WaitStatus::Exited(w.pid().unwrap(), 0)); - } else { - panic!("Failed to wait for process"); - } - - Ok(()) +test_channel! { + test_channel_files: "files", + test_channel_dirs: "dirs", + test_channel_env: "env", + test_channel_git_log: "git-log", + test_channel_git_reflog: "git-reflog", + test_channel_git_branch: "git-branch", + test_channel_text: "text", + test_channel_diff: "git-diff", }