test(e2e): more end to end tests

This commit is contained in:
alexandre pasmantier 2025-06-16 01:43:18 +02:00 committed by Alex Pasmantier
parent 8d822cd2fc
commit 3b57710006
4 changed files with 329 additions and 127 deletions

113
Cargo.lock generated
View File

@ -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"

View File

@ -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]

BIN
output.txt Normal file

Binary file not shown.

View File

@ -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<dyn portable_pty::Child + Send + Sync + 'static>,
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<D>(channels: Vec<&str>, cable_dir: D)
where
D: AsRef<Path>,
{
let cable_dir = cable_dir.as_ref();
std::fs::create_dir_all(cable_dir).unwrap();
for channel in channels {
let name = toml::from_str::<ChannelPrototype>(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<dyn portable_pty::Child + Send + Sync + 'static> =
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",
}