From 8d822cd2fcfdc3a00c612e30674391426e988040 Mon Sep 17 00:00:00 2001 From: alexandre pasmantier Date: Sun, 15 Jun 2025 18:34:55 +0200 Subject: [PATCH] test(e2e): add e2e tests for secondary cli commands (version, init, list-channels, ...) --- Cargo.lock | 63 +++++++++++++++++++++++--- Cargo.toml | 1 + television/cable.rs | 10 ++--- television/cli/mod.rs | 9 ++-- television/config/mod.rs | 6 +++ television/main.rs | 24 +++++----- tests/e2e.rs | 97 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 181 insertions(+), 29 deletions(-) create mode 100644 tests/e2e.rs diff --git a/Cargo.lock b/Cargo.lock index 6e20081..eaa955d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,12 @@ dependencies = [ "console", ] +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + [[package]] name = "bitflags" version = "2.9.0" @@ -341,6 +347,12 @@ 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" @@ -461,7 +473,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.9.0", "crossterm_winapi", "filedescriptor", "mio", @@ -963,7 +975,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.9.0", "libc", ] @@ -1019,6 +1031,15 @@ 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" @@ -1046,6 +1067,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.2.1", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nom" version = "7.1.3" @@ -1290,7 +1325,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags", + "bitflags 2.9.0", "cassowary", "compact_str", "crossterm", @@ -1332,7 +1367,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] @@ -1396,6 +1431,19 @@ 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" @@ -1434,7 +1482,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1447,7 +1495,7 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ - "bitflags", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.9.4", @@ -1742,6 +1790,7 @@ dependencies = [ "nucleo", "parking_lot", "ratatui", + "rexpect", "rustc-hash", "serde", "serde_json", @@ -2378,7 +2427,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ae2deef..ed9316d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ clipboard-win = "5.4.0" [dev-dependencies] criterion = { version = "0.5", features = ["async_tokio"] } tempfile = "3.16.0" +rexpect = "0.5" [build-dependencies] diff --git a/television/cable.rs b/television/cable.rs index 8ca0fa2..99faa78 100644 --- a/television/cable.rs +++ b/television/cable.rs @@ -10,7 +10,6 @@ use walkdir::WalkDir; use crate::{ channels::prototypes::ChannelPrototype, cli::unknown_channel_exit, - config::get_config_dir, }; /// A neat `HashMap` of channel prototypes indexed by their name. @@ -129,16 +128,13 @@ where /// ├── channel_2.toml /// └── ... /// ``` -pub fn load_cable

(cable_dir: Option

) -> Option +pub fn load_cable

(cable_dir: P) -> Option where P: AsRef, { - let cable_dir = match cable_dir { - Some(dir) => PathBuf::from(dir.as_ref()), - None => get_config_dir().join(CABLE_DIR_NAME), - }; + let cable_dir = cable_dir.as_ref(); debug!("Using cable directory: {}", cable_dir.to_string_lossy()); - let cable_files = get_cable_files(&cable_dir); + let cable_files = get_cable_files(cable_dir); debug!("Found cable channel files: {:?}", cable_files); if cable_files.is_empty() { diff --git a/television/cli/mod.rs b/television/cli/mod.rs index 1870307..d912606 100644 --- a/television/cli/mod.rs +++ b/television/cli/mod.rs @@ -156,9 +156,12 @@ fn parse_keybindings_literal( toml::from_str(&toml_definition).map_err(|e| anyhow!(e)) } -pub fn list_channels(cable_dir: Option<&Path>) { - let channels = cable::load_cable::<&Path>(cable_dir) - .expect("Failed to load cable channels"); +pub fn list_channels

(cable_dir: P) +where + P: AsRef, +{ + let channels = + cable::load_cable(cable_dir).expect("Failed to load cable channels"); for c in channels.keys() { println!("\t{c}"); } diff --git a/television/config/mod.rs b/television/config/mod.rs index 7c4081d..3753c93 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -35,6 +35,8 @@ pub struct AppConfig { pub data_dir: PathBuf, #[serde(default = "get_config_dir")] pub config_dir: PathBuf, + #[serde(default = "default_cable_dir")] + pub cable_dir: PathBuf, #[serde(default = "default_frame_rate")] pub frame_rate: f64, #[serde(default = "default_tick_rate")] @@ -290,6 +292,10 @@ pub fn get_config_dir() -> PathBuf { } } +fn default_cable_dir() -> PathBuf { + get_config_dir().join(CABLE_DIR_NAME) +} + fn project_directory() -> Option { ProjectDirs::from("com", "", env!("CARGO_PKG_NAME")) } diff --git a/television/main.rs b/television/main.rs index 6bf7792..4a328a2 100644 --- a/television/main.rs +++ b/television/main.rs @@ -47,22 +47,23 @@ async fn main() -> Result<()> { let mut config = Config::new(&ConfigEnv::init()?, args.config_file.as_deref())?; + // override configuration values with provided CLI arguments + debug!("Applying CLI overrides..."); + apply_cli_overrides(&args, &mut config); + // handle subcommands debug!("Handling subcommands..."); if let Some(subcommand) = &args.command { - handle_subcommand(subcommand, &config, &args)?; + handle_subcommand(subcommand, &config)?; } debug!("Loading cable channels..."); - let cable = load_cable(args.cable_dir.as_ref()).unwrap_or_else(|| exit(1)); + let cable = + load_cable(&config.application.cable_dir).unwrap_or_else(|| exit(1)); // optionally change the working directory args.working_directory.as_ref().map(set_current_dir); - // optionally override configuration values with CLI arguments - debug!("Applying CLI overrides..."); - apply_cli_overrides(&args, &mut config); - // determine the channel to use based on the CLI arguments and configuration debug!("Determining channel..."); let channel_prototype = @@ -106,6 +107,9 @@ async fn main() -> Result<()> { /// /// This function mutates the configuration in place. fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) { + if let Some(cable_dir) = &args.cable_dir { + config.application.cable_dir.clone_from(cable_dir); + } if let Some(tick_rate) = args.tick_rate { config.application.tick_rate = tick_rate; } @@ -140,14 +144,10 @@ pub fn set_current_dir(path: &String) -> Result<()> { Ok(()) } -pub fn handle_subcommand( - command: &Command, - config: &Config, - args: &PostProcessedCli, -) -> Result<()> { +pub fn handle_subcommand(command: &Command, config: &Config) -> Result<()> { match command { Command::ListChannels => { - list_channels(args.cable_dir.as_deref()); + list_channels(&config.application.cable_dir); exit(0); } Command::InitShell { shell } => { diff --git a/tests/e2e.rs b/tests/e2e.rs new file mode 100644 index 0000000..b6f4158 --- /dev/null +++ b/tests/e2e.rs @@ -0,0 +1,97 @@ +use std::path::Path; + +use rexpect::error::Error; +use rexpect::process::wait::WaitStatus; +use rexpect::spawn; +use television::channels::prototypes::ChannelPrototype; +use tempfile::TempDir; + +#[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 +} + +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() -> 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(()) +}